| @@ -1,6 +1,7 @@ | |||||
| v0.4 (unreleased): | v0.4 (unreleased): | ||||
| - Migrated to Python 3 (3.11+). Substantial code cleanup. | - Migrated to Python 3 (3.11+). Substantial code cleanup. | ||||
| - Migrated to pyproject.toml and pytest. | |||||
| - Migrated from oursql to pymysql. | - Migrated from oursql to pymysql. | ||||
| - Copyvios: Configurable proxy support for specific domains. | - Copyvios: Configurable proxy support for specific domains. | ||||
| - Copyvios: Parser-directed URL redirection. | - Copyvios: Parser-directed URL redirection. | ||||
| @@ -1,75 +1,84 @@ | |||||
| EarwigBot | EarwigBot | ||||
| ========= | ========= | ||||
| EarwigBot_ is a Python_ robot that edits Wikipedia_ and interacts with people | |||||
| over IRC_. This file provides a basic overview of how to install and setup the | |||||
| bot; more detailed information is located in the ``docs/`` directory (available | |||||
| online at PyPI_). | |||||
| EarwigBot_ is a Python bot that edits Wikipedia_ and interacts over IRC_. | |||||
| This README provides a basic overview of how to install and setup the bot; | |||||
| more detailed information is located in the ``docs/`` directory | |||||
| (`available online_`). | |||||
| History | History | ||||
| ------- | ------- | ||||
| Development began, based on `Pywikibot`_, in early 2009. Approval for its | Development began, based on `Pywikibot`_, in early 2009. Approval for its | ||||
| first task, a `copyright violation detector`_, was carried out in May, and the | first task, a `copyright violation detector`_, was carried out in May, and the | ||||
| bot has been running consistently ever since (with the exception of Jan/Feb 2011). | |||||
| It currently handles `several ongoing tasks`_ ranging from statistics generation | |||||
| to category cleanup, and on-demand tasks such as WikiProject template tagging. | |||||
| Since it started running, the bot has made over 250,000 edits. | |||||
| bot has been running consistently ever since. It currently handles | |||||
| `several ongoing tasks`_ ranging from statistics generation to category | |||||
| cleanup, and on-demand tasks such as WikiProject template tagging. Since it | |||||
| started running, the bot has made over 300,000 edits. | |||||
| A project to rewrite it from scratch began in early April 2011, thus moving | |||||
| away from the Pywikibot framework and allowing for less overall code, better | |||||
| integration between bot parts, and easier maintenance. | |||||
| The current version of its codebase began development in April 2011, moving | |||||
| away from Pywikibot to a custom framework. | |||||
| Installation | Installation | ||||
| ------------ | ------------ | ||||
| This package contains the core ``earwigbot``, abstracted enough that it should | |||||
| be usable and customizable by anyone running a bot on a MediaWiki site. Since | |||||
| it is component-based, the IRC components can be disabled if desired. IRC | |||||
| commands and bot tasks specific to `my instance of EarwigBot`_ that I don't | |||||
| feel the average user will need are available from the repository | |||||
| `earwigbot-plugins`_. | |||||
| It's recommended to run the bot's unit tests before installing. Run ``python | |||||
| 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. | |||||
| This package contains the core ``earwigbot``, abstracted to be usable and | |||||
| customizable by anyone running a bot on a MediaWiki site. Since it is modular, | |||||
| the IRC components can be disabled if desired. IRC commands and bot tasks | |||||
| specific to `my instance of EarwigBot`_ that I don't feel the average user | |||||
| will need are available from the repository `earwigbot-plugins`_. | |||||
| Latest release | Latest release | ||||
| ~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~ | ||||
| EarwigBot is available from the `Python Package Index`_, so you can install the | |||||
| latest release with ``pip install earwigbot``. | |||||
| EarwigBot is available from the `Python Package Index`_, so you can install | |||||
| the latest release with: | |||||
| pip install earwigbot | |||||
| There are a few sets of optional dependencies: | |||||
| - ``crypto``: Allows encrypting bot passwords and secrets in the config | |||||
| - ``sql``: Allows interfacing with MediaWiki databases (e.g. on Toolforge_) | |||||
| - ``copyvios``: Includes parsing libraries for checking copyright violations | |||||
| - ``dev``: Installs development dependencies (e.g. test runners) | |||||
| 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`_. | |||||
| For example, to install all non-dev dependencies: | |||||
| pip install 'earwigbot[crypto,sql,copyvios]' | |||||
| Errors while pip is installing dependencies may be due to missing header | |||||
| files. For example, on Ubuntu, see `this StackOverflow post`_. | |||||
| Development version | Development version | ||||
| ~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~ | ||||
| You can install the development version of the bot from ``git`` by using | |||||
| setuptools's ``develop`` command:: | |||||
| You can install the development version of the bot:: | |||||
| git clone git://github.com/earwig/earwigbot.git earwigbot | |||||
| git clone https://github.com/earwig/earwigbot.git | |||||
| cd earwigbot | cd earwigbot | ||||
| python setup.py develop | |||||
| python3 -m venv venv | |||||
| . venv/bin/activate | |||||
| pip install -e '.[crypto,sql,copyvios,dev]' | |||||
| To run the bot's unit tests, run ``pytest`` (requires the ``dev`` | |||||
| dependencies). Coverage is currently rather incomplete. | |||||
| Setup | Setup | ||||
| ----- | ----- | ||||
| The bot stores its data in a "working directory", including its config file and | |||||
| databases. This is also the location where you will place custom IRC commands | |||||
| and bot tasks, which will be explained later. It doesn't matter where this | |||||
| directory is, as long as the bot can write to it. | |||||
| The bot stores its data in a "working directory", including its config file | |||||
| and databases. This is also the location where you will place custom IRC | |||||
| commands and bot tasks, which will be explained later. It doesn't matter where | |||||
| this directory is, as long as the bot can write to it. | |||||
| Start the bot with ``earwigbot path/to/working/dir``, or just ``earwigbot`` if | Start the bot with ``earwigbot path/to/working/dir``, or just ``earwigbot`` if | ||||
| the working directory is the current directory. It will notice that no | the working directory is the current directory. It will notice that no | ||||
| ``config.yml`` file exists and take you through the setup process. | ``config.yml`` file exists and take you through the setup process. | ||||
| There is currently no way to edit the ``config.yml`` file from within the bot | There is currently no way to edit the ``config.yml`` file from within the bot | ||||
| after it has been created, but YAML is a very straightforward format, so you | |||||
| should be able to make any necessary changes yourself. Check out the | |||||
| `explanation of YAML`_ on Wikipedia for help. | |||||
| after it has been created, but you should be able to make any necessary | |||||
| changes yourself. | |||||
| After setup, the bot will start. This means it will connect to the IRC servers | After setup, the bot will start. This means it will connect to the IRC servers | ||||
| it has been configured for, schedule bot tasks to run at specific times, and | it has been configured for, schedule bot tasks to run at specific times, and | ||||
| @@ -86,8 +95,8 @@ Customizing | |||||
| The bot's working directory contains a ``commands`` subdirectory and a | The bot's working directory contains a ``commands`` subdirectory and a | ||||
| ``tasks`` subdirectory. Custom IRC commands can be placed in the former, | ``tasks`` subdirectory. Custom IRC commands can be placed in the former, | ||||
| whereas custom wiki bot tasks go into the latter. Developing custom modules is | whereas custom wiki bot tasks go into the latter. Developing custom modules is | ||||
| explained below, and in more detail through the bot's documentation on PyPI_ | |||||
| (or in the ``docs/`` dir). | |||||
| explained below, and in more detail through the bot's documentation_ or in the | |||||
| ``docs/`` dir. | |||||
| Note that custom commands will override built-in commands and tasks with the | Note that custom commands will override built-in commands and tasks with the | ||||
| same name. | same name. | ||||
| @@ -101,11 +110,11 @@ because it is the main way to communicate with other parts of the bot. A | |||||
| ``Bot`` object is accessible as an attribute of commands and tasks (i.e., | ``Bot`` object is accessible as an attribute of commands and tasks (i.e., | ||||
| ``self.bot``). | ``self.bot``). | ||||
| `earwigbot.config.BotConfig`_ stores configuration information for the bot. Its | |||||
| docstring explains what each attribute is used for, but essentially each "node" | |||||
| (one of ``config.components``, ``wiki``, ``irc``, ``commands``, ``tasks``, and | |||||
| ``metadata``) maps to a section of the bot's ``config.yml`` file. For example, | |||||
| if ``config.yml`` includes something like:: | |||||
| `earwigbot.config.BotConfig`_ stores configuration information for the bot. | |||||
| Its docstring explains what each attribute is used for, but essentially each | |||||
| "node" (one of ``config.components``, ``wiki``, ``irc``, ``commands``, | |||||
| ``tasks``, and ``metadata``) maps to a section of the bot's ``config.yml`` | |||||
| file. For example, if ``config.yml`` includes something like:: | |||||
| irc: | irc: | ||||
| frontend: | frontend: | ||||
| @@ -115,7 +124,7 @@ if ``config.yml`` includes something like:: | |||||
| - "#channel" | - "#channel" | ||||
| - "#other-channel" | - "#other-channel" | ||||
| ...then ``config.irc["frontend"]["nick"]`` will be ``"MyAwesomeBot"`` and | |||||
| then ``config.irc["frontend"]["nick"]`` will be ``"MyAwesomeBot"`` and | |||||
| ``config.irc["frontend"]["channels"]`` will be ``["##earwigbot", "#channel", | ``config.irc["frontend"]["channels"]`` will be ``["##earwigbot", "#channel", | ||||
| "#other-channel"]``. | "#other-channel"]``. | ||||
| @@ -133,8 +142,8 @@ afc_status_ for some more complicated scripts. | |||||
| Custom bot tasks | Custom bot tasks | ||||
| ~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~ | ||||
| Custom tasks are subclasses of `earwigbot.tasks.Task`_ that override ``Task``'s | |||||
| ``run()`` (and optionally ``setup()`` or ``unload()``) methods. | |||||
| Custom tasks are subclasses of `earwigbot.tasks.Task`_ that override | |||||
| ``Task``'s ``run()`` (and optionally ``setup()`` or ``unload()``) methods. | |||||
| See the built-in wikiproject_tagger_ task for a relatively straightforward | See the built-in wikiproject_tagger_ task for a relatively straightforward | ||||
| task, or the afc_statistics_ plugin for a more complicated one. | task, or the afc_statistics_ plugin for a more complicated one. | ||||
| @@ -142,10 +151,10 @@ task, or the afc_statistics_ plugin for a more complicated one. | |||||
| The Wiki Toolset | The Wiki Toolset | ||||
| ---------------- | ---------------- | ||||
| EarwigBot's answer to the `Pywikipedia framework`_ is the Wiki Toolset | |||||
| (``earwigbot.wiki``), which you will mainly access through ``bot.wiki``. | |||||
| EarwigBot's answer to the Pywikibot_ is the Wiki Toolset (``earwigbot.wiki``), | |||||
| which you will mainly access through ``bot.wiki``. | |||||
| ``bot.wiki`` provides three methods for the management of Sites - | |||||
| ``bot.wiki`` provides three methods for the management of Sites: | |||||
| ``get_site()``, ``add_site()``, and ``remove_site()``. Sites are objects that | ``get_site()``, ``add_site()``, and ``remove_site()``. Sites are objects that | ||||
| simply represent a MediaWiki site. A single instance of EarwigBot (i.e. a | simply represent a MediaWiki site. A single instance of EarwigBot (i.e. a | ||||
| single *working directory*) is expected to relate to a single site or group of | single *working directory*) is expected to relate to a single site or group of | ||||
| @@ -160,25 +169,25 @@ docstrings`_ to learn how to use it in a more hands-on fashion. For reference, | |||||
| ``sites.db`` file in the bot's working directory. | ``sites.db`` file in the bot's working directory. | ||||
| .. _EarwigBot: https://en.wikipedia.org/wiki/User:EarwigBot | .. _EarwigBot: https://en.wikipedia.org/wiki/User:EarwigBot | ||||
| .. _Python: https://python.org/ | |||||
| .. _Wikipedia: https://en.wikipedia.org/ | .. _Wikipedia: https://en.wikipedia.org/ | ||||
| .. _IRC: https://en.wikipedia.org/wiki/Internet_Relay_Chat | .. _IRC: https://en.wikipedia.org/wiki/Internet_Relay_Chat | ||||
| .. _PyPI: https://packages.python.org/earwigbot | |||||
| .. _available online: https://pythonhosted.org/earwigbot/ | |||||
| .. _Pywikibot: https://www.mediawiki.org/wiki/Manual:Pywikibot | .. _Pywikibot: https://www.mediawiki.org/wiki/Manual:Pywikibot | ||||
| .. _copyright violation detector: https://en.wikipedia.org/wiki/Wikipedia:Bots/Requests_for_approval/EarwigBot_1 | .. _copyright violation detector: https://en.wikipedia.org/wiki/Wikipedia:Bots/Requests_for_approval/EarwigBot_1 | ||||
| .. _several ongoing tasks: https://en.wikipedia.org/wiki/User:EarwigBot#Tasks | .. _several ongoing tasks: https://en.wikipedia.org/wiki/User:EarwigBot#Tasks | ||||
| .. _my instance of EarwigBot: https://en.wikipedia.org/wiki/User:EarwigBot | .. _my instance of EarwigBot: https://en.wikipedia.org/wiki/User:EarwigBot | ||||
| .. _earwigbot-plugins: https://github.com/earwig/earwigbot-plugins | .. _earwigbot-plugins: https://github.com/earwig/earwigbot-plugins | ||||
| .. _Python Package Index: https://pypi.python.org/pypi/earwigbot | .. _Python Package Index: https://pypi.python.org/pypi/earwigbot | ||||
| .. _Toolforge: https://wikitech.wikimedia.org/wiki/Portal:Toolforge | |||||
| .. _this StackOverflow post: https://stackoverflow.com/questions/6504810/how-to-install-lxml-on-ubuntu/6504860#6504860 | .. _this StackOverflow post: https://stackoverflow.com/questions/6504810/how-to-install-lxml-on-ubuntu/6504860#6504860 | ||||
| .. _explanation of YAML: https://en.wikipedia.org/wiki/YAML | |||||
| .. _earwigbot.bot.Bot: https://github.com/earwig/earwigbot/blob/develop/earwigbot/bot.py | |||||
| .. _earwigbot.config.BotConfig: https://github.com/earwig/earwigbot/blob/develop/earwigbot/config.py | |||||
| .. _earwigbot.commands.Command: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/__init__.py | |||||
| .. _test: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/test.py | |||||
| .. _chanops: https://github.com/earwig/earwigbot/blob/develop/earwigbot/commands/chanops.py | |||||
| .. _afc_status: https://github.com/earwig/earwigbot-plugins/blob/develop/commands/afc_status.py | |||||
| .. _earwigbot.tasks.Task: https://github.com/earwig/earwigbot/blob/develop/earwigbot/tasks/__init__.py | |||||
| .. _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 | |||||
| .. _documentation: https://pythonhosted.org/earwigbot/ | |||||
| .. _earwigbot.bot.Bot: https://github.com/earwig/earwigbot/blob/main/earwigbot/bot.py | |||||
| .. _earwigbot.config.BotConfig: https://github.com/earwig/earwigbot/blob/main/earwigbot/config.py | |||||
| .. _earwigbot.commands.Command: https://github.com/earwig/earwigbot/blob/main/earwigbot/commands/__init__.py | |||||
| .. _test: https://github.com/earwig/earwigbot/blob/main/earwigbot/commands/test.py | |||||
| .. _chanops: https://github.com/earwig/earwigbot/blob/main/earwigbot/commands/chanops.py | |||||
| .. _afc_status: https://github.com/earwig/earwigbot-plugins/blob/main/commands/afc_status.py | |||||
| .. _earwigbot.tasks.Task: https://github.com/earwig/earwigbot/blob/main/earwigbot/tasks/__init__.py | |||||
| .. _wikiproject_tagger: https://github.com/earwig/earwigbot/blob/main/earwigbot/tasks/wikiproject_tagger.py | |||||
| .. _afc_statistics: https://github.com/earwig/earwigbot-plugins/blob/main/tasks/afc_statistics.py | |||||
| .. _its code and docstrings: https://github.com/earwig/earwigbot/tree/main/earwigbot/wiki | |||||
| @@ -15,6 +15,13 @@ earwigbot Package | |||||
| :members: | :members: | ||||
| :undoc-members: | :undoc-members: | ||||
| :mod:`cli` Module | |||||
| ------------------ | |||||
| .. automodule:: earwigbot.cli | |||||
| :members: | |||||
| :undoc-members: | |||||
| :mod:`exceptions` Module | :mod:`exceptions` Module | ||||
| ------------------------ | ------------------------ | ||||
| @@ -38,13 +45,6 @@ earwigbot Package | |||||
| :undoc-members: | :undoc-members: | ||||
| :show-inheritance: | :show-inheritance: | ||||
| :mod:`util` Module | |||||
| .. automodule:: earwigbot.util | |||||
| :members: | |||||
| :undoc-members: | |||||
| Subpackages | Subpackages | ||||
| ----------- | ----------- | ||||
| @@ -226,7 +226,7 @@ texinfo_documents = [ | |||||
| "EarwigBot Documentation", | "EarwigBot Documentation", | ||||
| "Ben Kurtovic", | "Ben Kurtovic", | ||||
| "EarwigBot", | "EarwigBot", | ||||
| "EarwigBot is a Python robot that edits Wikipedia and interacts with people over IRC.", | |||||
| "EarwigBot is a bot that edits Wikipedia and interacts over IRC", | |||||
| "Miscellaneous", | "Miscellaneous", | ||||
| ), | ), | ||||
| ] | ] | ||||
| @@ -76,8 +76,8 @@ includes something like:: | |||||
| - "#channel" | - "#channel" | ||||
| - "#other-channel" | - "#other-channel" | ||||
| ...then :py:attr:`config.irc["frontend"]["nick"]` will be ``"MyAwesomeBot"`` | |||||
| and :py:attr:`config.irc["frontend"]["channels"]` will be | |||||
| then :py:attr:`config.irc["frontend"]["nick"]` will be ``"MyAwesomeBot"`` and | |||||
| :py:attr:`config.irc["frontend"]["channels"]` will be | |||||
| ``["##earwigbot", "#channel", "#other-channel"]``. | ``["##earwigbot", "#channel", "#other-channel"]``. | ||||
| Custom IRC commands | Custom IRC commands | ||||
| @@ -1,25 +1,22 @@ | |||||
| EarwigBot v0.4 Documentation | EarwigBot v0.4 Documentation | ||||
| ============================ | ============================ | ||||
| EarwigBot_ is a Python_ robot that edits Wikipedia_ and interacts with people | |||||
| over IRC_. | |||||
| EarwigBot_ is a Python bot that edits Wikipedia_ and interacts over IRC_. | |||||
| History | History | ||||
| ------- | ------- | ||||
| Development began, based on `Pywikibot`_, in early 2009. Approval for its | Development began, based on `Pywikibot`_, in early 2009. Approval for its | ||||
| first task, a `copyright violation detector`_, was carried out in May, and the | first task, a `copyright violation detector`_, was carried out in May, and the | ||||
| bot has been running consistently ever since (with the exception of Jan/Feb 2011). | |||||
| It currently handles `several ongoing tasks`_ ranging from statistics generation | |||||
| to category cleanup, and on-demand tasks such as WikiProject template tagging. | |||||
| Since it started running, the bot has made over 250,000 edits. | |||||
| bot has been running consistently ever since. It currently handles | |||||
| `several ongoing tasks`_ ranging from statistics generation to category | |||||
| cleanup, and on-demand tasks such as WikiProject template tagging. Since it | |||||
| started running, the bot has made over 300,000 edits. | |||||
| A project to rewrite it from scratch began in early April 2011, thus moving | |||||
| away from the Pywikibot framework and allowing for less overall code, better | |||||
| integration between bot parts, and easier maintenance. | |||||
| The current version of its codebase began development in April 2011, moving | |||||
| away from Pywikibot to a custom framework. | |||||
| .. _EarwigBot: https://en.wikipedia.org/wiki/User:EarwigBot | .. _EarwigBot: https://en.wikipedia.org/wiki/User:EarwigBot | ||||
| .. _Python: https://python.org/ | |||||
| .. _Wikipedia: https://en.wikipedia.org/ | .. _Wikipedia: https://en.wikipedia.org/ | ||||
| .. _IRC: https://en.wikipedia.org/wiki/Internet_Relay_Chat | .. _IRC: https://en.wikipedia.org/wiki/Internet_Relay_Chat | ||||
| .. _PyPI: https://packages.python.org/earwigbot | .. _PyPI: https://packages.python.org/earwigbot | ||||
| @@ -1,38 +1,50 @@ | |||||
| Installation | Installation | ||||
| ============ | ============ | ||||
| This package contains the core :py:mod:`earwigbot`, abstracted enough that it | |||||
| should be usable and customizable by anyone running a bot on a MediaWiki site. | |||||
| Since it is component-based, the IRC components can be disabled if desired. IRC | |||||
| commands and bot tasks specific to `my instance of EarwigBot`_ that I don't | |||||
| feel the average user will need are available from the repository | |||||
| `earwigbot-plugins`_. | |||||
| It's recommended to run the bot's unit tests before installing. Run | |||||
| :command:`python 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. | |||||
| This package contains the core :py:mod:`earwigbot`, abstracted to be usable | |||||
| and customizable by anyone running a bot on a MediaWiki site. Since it is | |||||
| modular, the IRC components can be disabled if desired. IRC commands and bot | |||||
| tasks specific to `my instance of EarwigBot`_ that I don't feel the average | |||||
| user will need are available from the repository `earwigbot-plugins`_. | |||||
| Latest release | Latest release | ||||
| -------------- | -------------- | ||||
| EarwigBot is available from the `Python Package Index`_, so you can install the | |||||
| latest release with :command:`pip install earwigbot`. | |||||
| EarwigBot is available from the `Python Package Index`_, so you can install | |||||
| the latest release with: | |||||
| 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`_. | |||||
| pip install earwigbot | |||||
| There are a few sets of optional dependencies: | |||||
| - ``crypto``: Allows encrypting bot passwords and secrets in the config | |||||
| - ``sql``: Allows interfacing with MediaWiki databases (e.g. on Toolforge_) | |||||
| - ``copyvios``: Includes parsing libraries for checking copyright violations | |||||
| - ``dev``: Installs development dependencies (e.g. test runners) | |||||
| For example, to install all non-dev dependencies: | |||||
| pip install 'earwigbot[crypto,sql,copyvios]' | |||||
| Errors while pip is installing dependencies may be due to missing header | |||||
| files. For example, on Ubuntu, see `this StackOverflow post`_. | |||||
| Development version | Development version | ||||
| ------------------- | ------------------- | ||||
| You can install the development version of the bot from :command:`git` by using | |||||
| setuptools's :command:`develop` command:: | |||||
| You can install the development version of the bot:: | |||||
| git clone git://github.com/earwig/earwigbot.git earwigbot | |||||
| git clone https://github.com/earwig/earwigbot.git | |||||
| cd earwigbot | cd earwigbot | ||||
| python setup.py develop | |||||
| python3 -m venv venv | |||||
| . venv/bin/activate | |||||
| pip install -e '.[crypto,sql,copyvios,dev]' | |||||
| To run the bot's unit tests, run :command:`pytest` (requires the ``dev`` | |||||
| dependencies). Coverage is currently rather incomplete. | |||||
| .. _my instance of EarwigBot: https://en.wikipedia.org/wiki/User:EarwigBot | .. _my instance of EarwigBot: https://en.wikipedia.org/wiki/User:EarwigBot | ||||
| .. _earwigbot-plugins: https://github.com/earwig/earwigbot-plugins | .. _earwigbot-plugins: https://github.com/earwig/earwigbot-plugins | ||||
| .. _Python Package Index: https://pypi.python.org/pypi/earwigbot | .. _Python Package Index: https://pypi.python.org/pypi/earwigbot | ||||
| .. _Toolforge: https://wikitech.wikimedia.org/wiki/Portal:Toolforge | |||||
| .. _this StackOverflow post: https://stackoverflow.com/questions/6504810/how-to-install-lxml-on-ubuntu/6504860#6504860 | .. _this StackOverflow post: https://stackoverflow.com/questions/6504810/how-to-install-lxml-on-ubuntu/6504860#6504860 | ||||
| @@ -1,20 +1,19 @@ | |||||
| Setup | Setup | ||||
| ===== | ===== | ||||
| The bot stores its data in a "working directory", including its config file and | |||||
| databases. This is also the location where you will place custom IRC commands | |||||
| and bot tasks, which will be explained later. It doesn't matter where this | |||||
| directory is, as long as the bot can write to it. | |||||
| The bot stores its data in a "working directory", including its config file | |||||
| and databases. This is also the location where you will place custom IRC | |||||
| commands and bot tasks, which will be explained later. It doesn't matter where | |||||
| this directory is, as long as the bot can write to it. | |||||
| Start the bot with :command:`earwigbot path/to/working/dir`, or just | Start the bot with :command:`earwigbot path/to/working/dir`, or just | ||||
| :command:`earwigbot` if the working directory is the current directory. It will | |||||
| notice that no :file:`config.yml` file exists and take you through the setup | |||||
| process. | |||||
| :command:`earwigbot` if the working directory is the current directory. It | |||||
| will notice that no :file:`config.yml` file exists and take you through the | |||||
| setup process. | |||||
| There is currently no way to edit the :file:`config.yml` file from within the | There is currently no way to edit the :file:`config.yml` file from within the | ||||
| bot after it has been created, but YAML is a very straightforward format, so | |||||
| you should be able to make any necessary changes yourself. Check out the | |||||
| `explanation of YAML`_ on Wikipedia for help. | |||||
| bot after it has been created, but you should be able to make any necessary | |||||
| changes yourself. | |||||
| After setup, the bot will start. This means it will connect to the IRC servers | After setup, the bot will start. This means it will connect to the IRC servers | ||||
| it has been configured for, schedule bot tasks to run at specific times, and | it has been configured for, schedule bot tasks to run at specific times, and | ||||
| @@ -24,5 +23,3 @@ then wait for instructions (as commands on IRC). For a list of commands, say | |||||
| You can stop the bot at any time with :kbd:`Control-c`, same as you stop a | You can stop the bot at any time with :kbd:`Control-c`, same as you stop a | ||||
| normal Python program, and it will try to exit safely. You can also use the | normal Python program, and it will try to exit safely. You can also use the | ||||
| "``!quit``" command on IRC. | "``!quit``" command on IRC. | ||||
| .. _explanation of YAML: https://en.wikipedia.org/wiki/YAML | |||||
| @@ -6,7 +6,7 @@ EarwigBot's answer to `Pywikibot`_ is the Wiki Toolset | |||||
| :py:attr:`bot.wiki <earwigbot.bot.Bot.wiki>`. | :py:attr:`bot.wiki <earwigbot.bot.Bot.wiki>`. | ||||
| :py:attr:`bot.wiki <earwigbot.bot.Bot.wiki>` provides three methods for the | :py:attr:`bot.wiki <earwigbot.bot.Bot.wiki>` provides three methods for the | ||||
| management of Sites - :py:meth:`~earwigbot.wiki.sitesdb.SitesDB.get_site`, | |||||
| management of Sites: :py:meth:`~earwigbot.wiki.sitesdb.SitesDB.get_site`, | |||||
| :py:meth:`~earwigbot.wiki.sitesdb.SitesDB.add_site`, and | :py:meth:`~earwigbot.wiki.sitesdb.SitesDB.add_site`, and | ||||
| :py:meth:`~earwigbot.wiki.sitesdb.SitesDB.remove_site`. Sites are objects that | :py:meth:`~earwigbot.wiki.sitesdb.SitesDB.remove_site`. Sites are objects that | ||||
| simply represent a MediaWiki site. A single instance of EarwigBot (i.e. a | simply represent a MediaWiki site. A single instance of EarwigBot (i.e. a | ||||
| @@ -1,3 +1,63 @@ | |||||
| [project] | |||||
| name = "earwigbot" | |||||
| version = "0.4.dev0" | |||||
| authors = [ | |||||
| {name = "Ben Kurtovic", email = "ben@benkurtovic.com"}, | |||||
| ] | |||||
| description = "EarwigBot is a bot that edits Wikipedia and interacts over IRC" | |||||
| readme = "README.rst" | |||||
| requires-python = ">=3.11" | |||||
| dependencies = [ | |||||
| "PyYAML >= 5.4.1", # Parsing config files | |||||
| "mwparserfromhell >= 0.6", # Parsing wikicode for manipulation | |||||
| "requests >= 2.25.1", # Wiki API requests | |||||
| "requests_oauthlib >= 1.3.0", # API authentication via OAuth | |||||
| ] | |||||
| keywords = ["earwig", "earwigbot", "irc", "wikipedia", "wiki", "mediawiki"] | |||||
| classifiers = [ | |||||
| "Development Status :: 3 - Alpha", | |||||
| "Environment :: Console", | |||||
| "Intended Audience :: Developers", | |||||
| "License :: OSI Approved :: MIT License", | |||||
| "Natural Language :: English", | |||||
| "Operating System :: OS Independent", | |||||
| "Programming Language :: Python :: 3", | |||||
| "Topic :: Communications :: Chat :: Internet Relay Chat", | |||||
| "Topic :: Internet :: WWW/HTTP", | |||||
| ] | |||||
| [project.optional-dependencies] | |||||
| crypto = [ | |||||
| "cryptography >= 3.4.7", # Storing bot passwords + keys in the config file | |||||
| ] | |||||
| sql = [ | |||||
| "pymysql >= 1.1.0", # Interfacing with MediaWiki databases | |||||
| ] | |||||
| copyvios = [ | |||||
| "beautifulsoup4 >= 4.9.3", # Parsing/scraping HTML | |||||
| "charset_normalizer >= 3.3.2", # Encoding detection for BeautifulSoup | |||||
| "lxml >= 4.6.3", # Faster parser for BeautifulSoup | |||||
| "nltk >= 3.6.1", # Parsing sentences to split article content | |||||
| "pdfminer >= 20191125", # Extracting text from PDF files | |||||
| "tldextract >= 3.1.0", # Getting domains for the multithreaded workers | |||||
| ] | |||||
| dev = [ | |||||
| "pytest >= 8.3.1" | |||||
| ] | |||||
| [project.urls] | |||||
| Homepage = "https://github.com/earwig/earwigbot" | |||||
| Documentation = "https://pythonhosted.org/earwigbot/" | |||||
| Issues = "https://github.com/earwig/earwigbot/issues" | |||||
| Changelog = "https://github.com/earwig/earwigbot/blob/main/CHANGELOG" | |||||
| [project.scripts] | |||||
| earwigbot = "earwigbot.cli:main" | |||||
| [build-system] | |||||
| requires = ["setuptools>=61.0"] | |||||
| build-backend = "setuptools.build_meta" | |||||
| [tool.ruff] | [tool.ruff] | ||||
| target-version = "py311" | target-version = "py311" | ||||
| @@ -1,84 +0,0 @@ | |||||
| #! /usr/bin/env python | |||||
| # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # | |||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
| # of this software and associated documentation files (the "Software"), to deal | |||||
| # in the Software without restriction, including without limitation the rights | |||||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
| # copies of the Software, and to permit persons to whom the Software is | |||||
| # furnished to do so, subject to the following conditions: | |||||
| # | |||||
| # The above copyright notice and this permission notice shall be included in | |||||
| # all copies or substantial portions of the Software. | |||||
| # | |||||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
| # SOFTWARE. | |||||
| from setuptools import find_packages, setup | |||||
| from earwigbot import __version__ | |||||
| required_deps = [ | |||||
| "PyYAML >= 5.4.1", # Parsing config files | |||||
| "mwparserfromhell >= 0.6", # Parsing wikicode for manipulation | |||||
| "requests >= 2.25.1", # Wiki API requests | |||||
| "requests_oauthlib >= 1.3.0", # API authentication via OAuth | |||||
| ] | |||||
| extra_deps = { | |||||
| "crypto": [ | |||||
| "cryptography >= 3.4.7", # Storing bot passwords + keys in the config file | |||||
| ], | |||||
| "sql": [ | |||||
| "pymysql >= 1.1.0", # Interfacing with MediaWiki databases | |||||
| ], | |||||
| "copyvios": [ | |||||
| "beautifulsoup4 >= 4.9.3", # Parsing/scraping HTML | |||||
| "charset_normalizer >= 3.3.2", # Encoding detection for BeautifulSoup | |||||
| "lxml >= 4.6.3", # Faster parser for BeautifulSoup | |||||
| "nltk >= 3.6.1", # Parsing sentences to split article content | |||||
| "pdfminer >= 20191125", # Extracting text from PDF files | |||||
| "tldextract >= 3.1.0", # Getting domains for the multithreaded workers | |||||
| ], | |||||
| "time": [ | |||||
| "pytz >= 2021.1", # Handling timezones for the !time IRC command | |||||
| ], | |||||
| } | |||||
| dependencies = required_deps + sum(extra_deps.values(), []) | |||||
| with open("README.rst") as fp: | |||||
| long_docs = fp.read() | |||||
| setup( | |||||
| name="earwigbot", | |||||
| packages=find_packages(exclude=("tests",)), | |||||
| entry_points={"console_scripts": ["earwigbot = earwigbot.util:main"]}, | |||||
| install_requires=dependencies, | |||||
| test_suite="tests", | |||||
| version=__version__, | |||||
| author="Ben Kurtovic", | |||||
| author_email="ben.kurtovic@gmail.com", | |||||
| url="https://github.com/earwig/earwigbot", | |||||
| description="EarwigBot is a Python robot that edits Wikipedia and interacts with people over IRC.", | |||||
| long_description=long_docs, | |||||
| download_url=f"https://github.com/earwig/earwigbot/tarball/v{__version__}", | |||||
| keywords="earwig earwigbot irc wikipedia wiki mediawiki", | |||||
| license="MIT License", | |||||
| classifiers=[ | |||||
| "Development Status :: 3 - Alpha", | |||||
| "Environment :: Console", | |||||
| "Intended Audience :: Developers", | |||||
| "License :: OSI Approved :: MIT License", | |||||
| "Natural Language :: English", | |||||
| "Operating System :: OS Independent", | |||||
| "Programming Language :: Python :: 3", | |||||
| "Topic :: Communications :: Chat :: Internet Relay Chat", | |||||
| "Topic :: Internet :: WWW/HTTP", | |||||
| ], | |||||
| ) | |||||
| @@ -61,23 +61,23 @@ importer = lazy.LazyImporter() | |||||
| if typing.TYPE_CHECKING: | if typing.TYPE_CHECKING: | ||||
| from earwigbot import ( | from earwigbot import ( | ||||
| bot, | bot, | ||||
| cli, | |||||
| commands, | commands, | ||||
| config, | config, | ||||
| exceptions, | exceptions, | ||||
| irc, | irc, | ||||
| managers, | managers, | ||||
| tasks, | tasks, | ||||
| util, | |||||
| wiki, | wiki, | ||||
| ) | ) | ||||
| else: | else: | ||||
| bot = importer.new("earwigbot.bot") | bot = importer.new("earwigbot.bot") | ||||
| cli = importer.new("earwigbot.cli") | |||||
| commands = importer.new("earwigbot.commands") | commands = importer.new("earwigbot.commands") | ||||
| config = importer.new("earwigbot.config") | config = importer.new("earwigbot.config") | ||||
| exceptions = importer.new("earwigbot.exceptions") | exceptions = importer.new("earwigbot.exceptions") | ||||
| irc = importer.new("earwigbot.irc") | irc = importer.new("earwigbot.irc") | ||||
| managers = importer.new("earwigbot.managers") | managers = importer.new("earwigbot.managers") | ||||
| tasks = importer.new("earwigbot.tasks") | tasks = importer.new("earwigbot.tasks") | ||||
| util = importer.new("earwigbot.util") | |||||
| wiki = importer.new("earwigbot.wiki") | wiki = importer.new("earwigbot.wiki") | ||||
| @@ -59,7 +59,7 @@ class Bot: | |||||
| :py:meth:`bot.wiki.get_site() <earwigbot.wiki.sitesdb.SitesDB.get_site>`. | :py:meth:`bot.wiki.get_site() <earwigbot.wiki.sitesdb.SitesDB.get_site>`. | ||||
| """ | """ | ||||
| def __init__(self, root_dir, level=logging.INFO): | |||||
| def __init__(self, root_dir: str, level=logging.INFO): | |||||
| self.config = BotConfig(self, root_dir, level) | self.config = BotConfig(self, root_dir, level) | ||||
| self.logger = logging.getLogger("earwigbot") | self.logger = logging.getLogger("earwigbot") | ||||
| self.commands = CommandManager(self) | self.commands = CommandManager(self) | ||||
| @@ -64,6 +64,7 @@ class _StoreTaskArg(Action): | |||||
| def __call__(self, parser, namespace, values, option_string=None): | def __call__(self, parser, namespace, values, option_string=None): | ||||
| kwargs = {} | kwargs = {} | ||||
| name = None | name = None | ||||
| assert isinstance(values, list | tuple), values | |||||
| for value in values: | for value in values: | ||||
| if value.startswith("-") and "=" in value: | if value.startswith("-") and "=" in value: | ||||
| key, value = value.split("=", 1) | key, value = value.split("=", 1) | ||||
| @@ -1,4 +1,4 @@ | |||||
| # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # | # | ||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| # of this software and associated documentation files (the "Software"), to deal | # of this software and associated documentation files (the "Software"), to deal | ||||
| @@ -18,14 +18,13 @@ | |||||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| # SOFTWARE. | # SOFTWARE. | ||||
| from datetime import datetime | |||||
| from math import floor | |||||
| from time import time | |||||
| import math | |||||
| import time | |||||
| from datetime import datetime, timezone | |||||
| from zoneinfo import ZoneInfo | |||||
| from earwigbot import importer | |||||
| from earwigbot.commands import Command | from earwigbot.commands import Command | ||||
| pytz = importer.new("pytz") | |||||
| from earwigbot.irc import Data | |||||
| class Time(Command): | class Time(Command): | ||||
| @@ -35,12 +34,12 @@ class Time(Command): | |||||
| name = "time" | name = "time" | ||||
| commands = ["time", "beats", "swatch", "epoch", "date"] | commands = ["time", "beats", "swatch", "epoch", "date"] | ||||
| def process(self, data): | |||||
| def process(self, data: Data) -> None: | |||||
| if data.command in ["beats", "swatch"]: | if data.command in ["beats", "swatch"]: | ||||
| self.do_beats(data) | self.do_beats(data) | ||||
| return | return | ||||
| if data.command == "epoch": | if data.command == "epoch": | ||||
| self.reply(data, time()) | |||||
| self.reply(data, time.time()) | |||||
| return | return | ||||
| if data.args: | if data.args: | ||||
| timezone = data.args[0] | timezone = data.args[0] | ||||
| @@ -51,20 +50,16 @@ class Time(Command): | |||||
| else: | else: | ||||
| self.do_time(data, timezone) | self.do_time(data, timezone) | ||||
| def do_beats(self, data): | |||||
| beats = ((time() + 3600) % 86400) / 86.4 | |||||
| beats = int(floor(beats)) | |||||
| def do_beats(self, data: Data) -> None: | |||||
| beats = ((time.time() + 3600) % 86400) / 86.4 | |||||
| beats = int(math.floor(beats)) | |||||
| self.reply(data, f"@{beats:0>3}") | self.reply(data, f"@{beats:0>3}") | ||||
| def do_time(self, data, timezone): | |||||
| def do_time(self, data: Data, tzname: str) -> None: | |||||
| try: | try: | ||||
| tzinfo = pytz.timezone(timezone) | |||||
| except ImportError: | |||||
| msg = "This command requires the 'pytz' package: https://pypi.org/project/pytz/" | |||||
| self.reply(data, msg) | |||||
| return | |||||
| except pytz.exceptions.UnknownTimeZoneError: | |||||
| self.reply(data, f"Unknown timezone: {timezone}.") | |||||
| tzinfo = ZoneInfo(tzname) | |||||
| except LookupError: | |||||
| self.reply(data, f"Unknown timezone: {timezone}") | |||||
| return | return | ||||
| now = pytz.utc.localize(datetime.utcnow()).astimezone(tzinfo) | |||||
| now = datetime.now(tz=tzinfo) | |||||
| self.reply(data, now.strftime("%Y-%m-%d %H:%M:%S %Z")) | self.reply(data, now.strftime("%Y-%m-%d %H:%M:%S %Z")) | ||||
| @@ -263,7 +263,9 @@ class TaskManager(_ResourceManager): | |||||
| msg = "Task '{0}' finished successfully" | msg = "Task '{0}' finished successfully" | ||||
| self.logger.info(msg.format(task.name)) | self.logger.info(msg.format(task.name)) | ||||
| if kwargs.get("fromIRC"): | if kwargs.get("fromIRC"): | ||||
| kwargs.get("_IRCCallback")() | |||||
| callback = kwargs.get("_IRCCallback") | |||||
| assert callable(callback), callback | |||||
| callback() | |||||
| def start(self, task_name, **kwargs): | def start(self, task_name, **kwargs): | ||||
| """Start a given task in a new daemon thread, and return the thread. | """Start a given task in a new daemon thread, and return the thread. | ||||
| @@ -299,7 +301,9 @@ class TaskManager(_ResourceManager): | |||||
| ) | ) | ||||
| for task in tasks: | for task in tasks: | ||||
| if isinstance(task, list): # They've specified kwargs, | |||||
| self.start(task[0], **task[1]) # so pass those to start | |||||
| else: # Otherwise, just pass task_name | |||||
| if isinstance(task, list): | |||||
| # They've specified kwargs, so pass those to start | |||||
| self.start(task[0], **task[1]) | |||||
| else: | |||||
| # Otherwise, just pass task_name | |||||
| self.start(task) | self.start(task) | ||||
| @@ -1,147 +0,0 @@ | |||||
| # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # | |||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
| # of this software and associated documentation files (the "Software"), to deal | |||||
| # in the Software without restriction, including without limitation the rights | |||||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
| # copies of the Software, and to permit persons to whom the Software is | |||||
| # furnished to do so, subject to the following conditions: | |||||
| # | |||||
| # The above copyright notice and this permission notice shall be included in | |||||
| # all copies or substantial portions of the Software. | |||||
| # | |||||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
| # SOFTWARE. | |||||
| """ | |||||
| EarwigBot's Unit Tests | |||||
| This __init__ file provides some support code for unit tests. | |||||
| Test cases: | |||||
| -- CommandTestCase provides setUp() for creating a fake connection, plus | |||||
| some other helpful methods for testing IRC commands. | |||||
| Fake objects: | |||||
| -- FakeBot implements Bot, using the Fake* equivalents of all objects | |||||
| whenever possible. | |||||
| -- FakeBotConfig implements BotConfig with silent logging. | |||||
| -- FakeIRCConnection implements IRCConnection, using an internal string | |||||
| buffer for data instead of sending it over a socket. | |||||
| """ | |||||
| import logging | |||||
| import re | |||||
| from os import path | |||||
| from threading import Lock | |||||
| from unittest import TestCase | |||||
| from earwigbot.bot import Bot | |||||
| from earwigbot.commands import CommandManager | |||||
| from earwigbot.config import BotConfig | |||||
| from earwigbot.irc import Data, IRCConnection | |||||
| from earwigbot.tasks import TaskManager | |||||
| from earwigbot.wiki import SitesDB | |||||
| class CommandTestCase(TestCase): | |||||
| re_sender = re.compile(":(.*?)!(.*?)@(.*?)\Z") | |||||
| def setUp(self, command): | |||||
| self.bot = FakeBot(path.dirname(__file__)) | |||||
| self.command = command(self.bot) | |||||
| self.command.connection = self.connection = self.bot.frontend | |||||
| def get_single(self): | |||||
| data = self.connection._get().split("\n") | |||||
| line = data.pop(0) | |||||
| for remaining in data[1:]: | |||||
| self.connection.send(remaining) | |||||
| return line | |||||
| def assertSent(self, msg): | |||||
| line = self.get_single() | |||||
| self.assertEqual(line, msg) | |||||
| def assertSentIn(self, msgs): | |||||
| line = self.get_single() | |||||
| self.assertIn(line, msgs) | |||||
| def assertSaid(self, msg): | |||||
| self.assertSent(f"PRIVMSG #channel :{msg}") | |||||
| def assertSaidIn(self, msgs): | |||||
| msgs = [f"PRIVMSG #channel :{msg}" for msg in msgs] | |||||
| self.assertSentIn(msgs) | |||||
| def assertReply(self, msg): | |||||
| self.assertSaid(f"\x02Foo\x0f: {msg}") | |||||
| def assertReplyIn(self, msgs): | |||||
| msgs = [f"\x02Foo\x0f: {msg}" for msg in msgs] | |||||
| self.assertSaidIn(msgs) | |||||
| def maker(self, line, chan, msg=None): | |||||
| data = Data(line) | |||||
| data.nick, data.ident, data.host = self.re_sender.findall(line[0])[0] | |||||
| if msg is not None: | |||||
| data.msg = msg | |||||
| data.chan = chan | |||||
| data.parse_args() | |||||
| return data | |||||
| def make_msg(self, command, *args): | |||||
| line = f":Foo!bar@example.com PRIVMSG #channel :!{command}" | |||||
| line = line.strip().split() | |||||
| line.extend(args) | |||||
| return self.maker(line, line[2], " ".join(line[3:])[1:]) | |||||
| def make_join(self): | |||||
| line = ":Foo!bar@example.com JOIN :#channel".strip().split() | |||||
| return self.maker(line, line[2][1:]) | |||||
| class FakeBot(Bot): | |||||
| def __init__(self, root_dir): | |||||
| self.config = FakeBotConfig(root_dir) | |||||
| self.logger = logging.getLogger("earwigbot") | |||||
| self.commands = CommandManager(self) | |||||
| self.tasks = TaskManager(self) | |||||
| self.wiki = SitesDB(self) | |||||
| self.frontend = FakeIRCConnection(self) | |||||
| self.watcher = FakeIRCConnection(self) | |||||
| self.component_lock = Lock() | |||||
| self._keep_looping = True | |||||
| class FakeBotConfig(BotConfig): | |||||
| def _setup_logging(self): | |||||
| logger = logging.getLogger("earwigbot") | |||||
| logger.addHandler(logging.NullHandler()) | |||||
| class FakeIRCConnection(IRCConnection): | |||||
| def __init__(self, bot): | |||||
| self.bot = bot | |||||
| self._is_running = False | |||||
| self._connect() | |||||
| def _connect(self): | |||||
| self._buffer = "" | |||||
| def _close(self): | |||||
| self._buffer = "" | |||||
| def _get(self, size=4096): | |||||
| data, self._buffer = self._buffer, "" | |||||
| return data | |||||
| def _send(self, msg): | |||||
| self._buffer += msg + "\n" | |||||
| @@ -0,0 +1,151 @@ | |||||
| # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # | |||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
| # of this software and associated documentation files (the "Software"), to deal | |||||
| # in the Software without restriction, including without limitation the rights | |||||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
| # copies of the Software, and to permit persons to whom the Software is | |||||
| # furnished to do so, subject to the following conditions: | |||||
| # | |||||
| # The above copyright notice and this permission notice shall be included in | |||||
| # all copies or substantial portions of the Software. | |||||
| # | |||||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
| # SOFTWARE. | |||||
| """ | |||||
| EarwigBot's Unit Tests | |||||
| This conftest provides some support code for unit tests. | |||||
| Fixtures: | |||||
| -- ``command`` creates a mock connection and provides some helpful methods for | |||||
| testing IRC commands. | |||||
| Mock objects: | |||||
| -- ``MockBot`` implements ``Bot``, using the ``Mock`` equivalents of all objects | |||||
| whenever possible. | |||||
| -- ``MockBotConfig`` implements ``BotConfig`` with silent logging. | |||||
| -- ``MockIRCConnection`` implements ``IRCConnection``, using an internal string | |||||
| buffer for data instead of sending it over a socket. | |||||
| """ | |||||
| import logging | |||||
| import os.path | |||||
| import re | |||||
| from collections.abc import Iterable, Sequence | |||||
| from threading import Lock | |||||
| import pytest | |||||
| from earwigbot.bot import Bot | |||||
| from earwigbot.commands import Command | |||||
| from earwigbot.config import BotConfig | |||||
| from earwigbot.irc import Data, IRCConnection | |||||
| from earwigbot.managers import CommandManager, TaskManager | |||||
| from earwigbot.wiki import SitesDB | |||||
| @pytest.fixture | |||||
| def command(): | |||||
| return MockCommand() | |||||
| class MockCommand: | |||||
| re_sender = re.compile(r":(.*?)!(.*?)@(.*?)\Z") | |||||
| def setup(self, command: type[Command]) -> None: | |||||
| self.bot = MockBot(os.path.dirname(__file__)) | |||||
| self.command = command(self.bot) | |||||
| def get_single(self) -> str: | |||||
| data = self.bot.frontend._get().split("\n") | |||||
| line = data.pop(0) | |||||
| for remaining in data[1:]: | |||||
| self.bot.frontend._send(remaining) | |||||
| return line | |||||
| def assert_sent(self, msg: str) -> None: | |||||
| line = self.get_single() | |||||
| assert line == msg | |||||
| def assert_sent_in(self, msgs: Iterable[str]) -> None: | |||||
| line = self.get_single() | |||||
| assert line in msgs | |||||
| def assert_said(self, msg: str) -> None: | |||||
| self.assert_sent(f"PRIVMSG #channel :{msg}") | |||||
| def assert_said_in(self, msgs: Iterable[str]) -> None: | |||||
| msgs = [f"PRIVMSG #channel :{msg}" for msg in msgs] | |||||
| self.assert_sent_in(msgs) | |||||
| def assert_reply(self, msg: str) -> None: | |||||
| self.assert_said(f"\x02Foo\x0f: {msg}") | |||||
| def assert_reply_in(self, msgs: Iterable[str]) -> None: | |||||
| msgs = [f"\x02Foo\x0f: {msg}" for msg in msgs] | |||||
| self.assert_said_in(msgs) | |||||
| def _make(self, line: Sequence[str]) -> Data: | |||||
| return Data(self.bot.frontend.nick, line, line[1]) | |||||
| def make_msg(self, command, *args): | |||||
| line = f":Foo!bar@example.com PRIVMSG #channel :!{command}" | |||||
| line = line.strip().split() | |||||
| line.extend(args) | |||||
| return self._make(line) | |||||
| def make_join(self): | |||||
| line = ":Foo!bar@example.com JOIN :#channel".strip().split() | |||||
| return self._make(line) | |||||
| class MockBot(Bot): | |||||
| def __init__(self, root_dir: str, level=logging.INFO) -> None: | |||||
| self.config = MockBotConfig(self, root_dir, level) | |||||
| self.logger = logging.getLogger("earwigbot") | |||||
| self.commands = CommandManager(self) | |||||
| self.tasks = TaskManager(self) | |||||
| self.wiki = SitesDB(self) | |||||
| self.frontend = MockIRCConnection(self) | |||||
| self.watcher = MockIRCConnection(self) | |||||
| self.component_lock = Lock() | |||||
| self._keep_looping = True | |||||
| class MockBotConfig(BotConfig): | |||||
| def _setup_logging(self) -> None: | |||||
| logger = logging.getLogger("earwigbot") | |||||
| logger.addHandler(logging.NullHandler()) | |||||
| class MockIRCConnection(IRCConnection): | |||||
| def __init__(self, bot: MockBot) -> None: | |||||
| super().__init__( | |||||
| "localhost", | |||||
| 6667, | |||||
| "MockBot", | |||||
| "mock", | |||||
| "Mock Bot", | |||||
| bot.logger.getChild("mock"), | |||||
| ) | |||||
| self._buffer = "" | |||||
| def _connect(self) -> None: | |||||
| self._buffer = "" | |||||
| def _close(self) -> None: | |||||
| self._buffer = "" | |||||
| def _get(self, size: int = 4096) -> str: | |||||
| data, self._buffer = self._buffer, "" | |||||
| return data | |||||
| def _send(self, msg: str, hidelog: bool = False) -> None: | |||||
| self._buffer += msg + "\n" | |||||
| @@ -1,4 +1,4 @@ | |||||
| # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # | # | ||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| # of this software and associated documentation files (the "Software"), to deal | # of this software and associated documentation files (the "Software"), to deal | ||||
| @@ -18,41 +18,41 @@ | |||||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| # SOFTWARE. | # SOFTWARE. | ||||
| import unittest | |||||
| import pytest | |||||
| from conftest import MockCommand | |||||
| from earwigbot.commands.calc import Calc | |||||
| from earwigbot.commands.calc import Command | |||||
| from tests import CommandTestCase | |||||
| def test_check(command: MockCommand): | |||||
| command.setup(Calc) | |||||
| class TestCalc(CommandTestCase): | |||||
| def setUp(self): | |||||
| super().setUp(Command) | |||||
| assert command.command.check(command.make_msg("bloop")) is False | |||||
| assert command.command.check(command.make_join()) is False | |||||
| def test_check(self): | |||||
| self.assertFalse(self.command.check(self.make_msg("bloop"))) | |||||
| self.assertFalse(self.command.check(self.make_join())) | |||||
| assert command.command.check(command.make_msg("calc")) is True | |||||
| assert command.command.check(command.make_msg("CALC", "foo")) is True | |||||
| self.assertTrue(self.command.check(self.make_msg("calc"))) | |||||
| self.assertTrue(self.command.check(self.make_msg("CALC", "foo"))) | |||||
| def test_ignore_empty(self): | |||||
| self.command.process(self.make_msg("calc")) | |||||
| self.assertReply("what do you want me to calculate?") | |||||
| def test_ignore_empty(command: MockCommand): | |||||
| command.setup(Calc) | |||||
| def test_maths(self): | |||||
| tests = [ | |||||
| ("2 + 2", "2 + 2 = 4"), | |||||
| ("13 * 5", "13 * 5 = 65"), | |||||
| ("80 / 42", "80 / 42 = 40/21 (approx. 1.9047619047619047)"), | |||||
| ("2/0", "2/0 = undef"), | |||||
| ("π", "π = 3.141592653589793238"), | |||||
| ] | |||||
| command.command.process(command.make_msg("calc")) | |||||
| command.assert_reply("What do you want me to calculate?") | |||||
| for test in tests: | |||||
| q = test[0].strip().split() | |||||
| self.command.process(self.make_msg("calc", *q)) | |||||
| self.assertReply(test[1]) | |||||
| @pytest.mark.parametrize( | |||||
| "expr, expected", | |||||
| [ | |||||
| ("2 + 2", "2 + 2 = 4"), | |||||
| ("13 * 5", "13 * 5 = 65"), | |||||
| ("80 / 42", "80 / 42 = 40/21 (approx. 1.9047619047619048)"), | |||||
| ("2/0", "2/0 = undef"), | |||||
| ("π", "π = 3.141592653589793238"), | |||||
| ], | |||||
| ) | |||||
| def test_math(command: MockCommand, expr: str, expected: str): | |||||
| command.setup(Calc) | |||||
| if __name__ == "__main__": | |||||
| unittest.main(verbosity=2) | |||||
| q = expr.strip().split() | |||||
| command.command.process(command.make_msg("calc", *q)) | |||||
| command.assert_reply(expected) | |||||
| @@ -1,4 +1,4 @@ | |||||
| # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2009-2024 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # | # | ||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| # of this software and associated documentation files (the "Software"), to deal | # of this software and associated documentation files (the "Software"), to deal | ||||
| @@ -18,31 +18,23 @@ | |||||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| # SOFTWARE. | # SOFTWARE. | ||||
| import unittest | |||||
| from conftest import MockCommand | |||||
| from earwigbot.commands.test import Test | |||||
| from earwigbot.commands.test import Command | |||||
| from tests import CommandTestCase | |||||
| def test_check(command: MockCommand): | |||||
| command.setup(Test) | |||||
| class TestTest(CommandTestCase): | |||||
| def setUp(self): | |||||
| super().setUp(Command) | |||||
| assert command.command.check(command.make_msg("bloop")) is False | |||||
| assert command.command.check(command.make_join()) is False | |||||
| def test_check(self): | |||||
| self.assertFalse(self.command.check(self.make_msg("bloop"))) | |||||
| self.assertFalse(self.command.check(self.make_join())) | |||||
| assert command.command.check(command.make_msg("test")) is True | |||||
| assert command.command.check(command.make_msg("TEST", "foo")) is True | |||||
| self.assertTrue(self.command.check(self.make_msg("test"))) | |||||
| self.assertTrue(self.command.check(self.make_msg("TEST", "foo"))) | |||||
| def test_process(self): | |||||
| def test(): | |||||
| self.command.process(self.make_msg("test")) | |||||
| self.assertSaidIn(["Hey \x02Foo\x0f!", "'sup \x02Foo\x0f?"]) | |||||
| def test_process(command: MockCommand): | |||||
| command.setup(Test) | |||||
| for i in range(64): | |||||
| test() | |||||
| if __name__ == "__main__": | |||||
| unittest.main(verbosity=2) | |||||
| for i in range(64): | |||||
| command.command.process(command.make_msg("test")) | |||||
| command.assert_said_in(["Hey \x02Foo\x0f!", "'Sup \x02Foo\x0f?"]) | |||||