| @@ -27,6 +27,7 @@ from earwigbot.commands import CommandManager | |||||
| from earwigbot.config import BotConfig | from earwigbot.config import BotConfig | ||||
| from earwigbot.irc import Frontend, Watcher | from earwigbot.irc import Frontend, Watcher | ||||
| from earwigbot.tasks import TaskManager | from earwigbot.tasks import TaskManager | ||||
| from earwigbot.wiki import SitesDBManager | |||||
| __all__ = ["Bot"] | __all__ = ["Bot"] | ||||
| @@ -51,6 +52,7 @@ class Bot(object): | |||||
| self.logger = logging.getLogger("earwigbot") | self.logger = logging.getLogger("earwigbot") | ||||
| self.commands = CommandManager(self) | self.commands = CommandManager(self) | ||||
| self.tasks = TaskManager(self) | self.tasks = TaskManager(self) | ||||
| self.wiki = SitesDBManager(self.config) | |||||
| self.frontend = None | self.frontend = None | ||||
| self.watcher = None | self.watcher = None | ||||
| @@ -100,12 +102,13 @@ class Bot(object): | |||||
| sleep(5) | sleep(5) | ||||
| def run(self): | def run(self): | ||||
| self.config.load() | |||||
| self.config.decrypt(config.wiki, "password") | |||||
| self.config.decrypt(config.wiki, "search", "credentials", "key") | |||||
| self.config.decrypt(config.wiki, "search", "credentials", "secret") | |||||
| self.config.decrypt(config.irc, "frontend", "nickservPassword") | |||||
| self.config.decrypt(config.irc, "watcher", "nickservPassword") | |||||
| config = self.config | |||||
| config.load() | |||||
| config.decrypt(config.wiki, "password") | |||||
| config.decrypt(config.wiki, "search", "credentials", "key") | |||||
| config.decrypt(config.wiki, "search", "credentials", "secret") | |||||
| config.decrypt(config.irc, "frontend", "nickservPassword") | |||||
| config.decrypt(config.irc, "watcher", "nickservPassword") | |||||
| self.commands.load() | self.commands.load() | ||||
| self.tasks.load() | self.tasks.load() | ||||
| self._start_irc_components() | self._start_irc_components() | ||||
| @@ -121,6 +124,7 @@ class Bot(object): | |||||
| self._start_irc_components() | self._start_irc_components() | ||||
| def stop(self): | def stop(self): | ||||
| self._stop_irc_components() | |||||
| with self.component_lock: | |||||
| self._stop_irc_components() | |||||
| self._keep_looping = False | self._keep_looping = False | ||||
| sleep(3) # Give a few seconds to finish closing IRC connections | sleep(3) # Give a few seconds to finish closing IRC connections | ||||
| @@ -58,9 +58,10 @@ class BaseCommand(object): | |||||
| super(Command, self).__init__() first. | super(Command, self).__init__() first. | ||||
| """ | """ | ||||
| self.bot = bot | self.bot = bot | ||||
| self.config = bot.config | |||||
| self.logger = bot.commands.getLogger(self.name) | self.logger = bot.commands.getLogger(self.name) | ||||
| def _execute(self, data): | |||||
| def _wrap_process(self, data): | |||||
| """Make a quick connection alias and then process() the message.""" | """Make a quick connection alias and then process() the message.""" | ||||
| self.connection = self.bot.frontend | self.connection = self.bot.frontend | ||||
| self.process(data) | self.process(data) | ||||
| @@ -158,7 +159,7 @@ class CommandManager(object): | |||||
| if hook in command.hooks: | if hook in command.hooks: | ||||
| if command.check(data): | if command.check(data): | ||||
| try: | try: | ||||
| command._execute(data) | |||||
| command._wrap_process(data) | |||||
| except Exception: | except Exception: | ||||
| e = "Error executing command '{0}':" | e = "Error executing command '{0}':" | ||||
| self.logger.exception(e.format(data.command)) | self.logger.exception(e.format(data.command)) | ||||
| @@ -24,7 +24,6 @@ import re | |||||
| from earwigbot import wiki | from earwigbot import wiki | ||||
| from earwigbot.commands import BaseCommand | from earwigbot.commands import BaseCommand | ||||
| from earwigbot.config import config | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Get the number of pending AfC submissions, open redirect requests, and | """Get the number of pending AfC submissions, open redirect requests, and | ||||
| @@ -39,7 +38,7 @@ class Command(BaseCommand): | |||||
| try: | try: | ||||
| if data.line[1] == "JOIN" and data.chan == "#wikipedia-en-afc": | if data.line[1] == "JOIN" and data.chan == "#wikipedia-en-afc": | ||||
| if data.nick != config.irc["frontend"]["nick"]: | |||||
| if data.nick != self.config.irc["frontend"]["nick"]: | |||||
| return True | return True | ||||
| except IndexError: | except IndexError: | ||||
| pass | pass | ||||
| @@ -21,7 +21,6 @@ | |||||
| # SOFTWARE. | # SOFTWARE. | ||||
| from earwigbot.commands import BaseCommand | from earwigbot.commands import BaseCommand | ||||
| from earwigbot.config import config | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Voice, devoice, op, or deop users in the channel.""" | """Voice, devoice, op, or deop users in the channel.""" | ||||
| @@ -39,7 +38,7 @@ class Command(BaseCommand): | |||||
| self.connection.reply(data, msg) | self.connection.reply(data, msg) | ||||
| return | return | ||||
| if data.host not in config.irc["permissions"]["admins"]: | |||||
| if data.host not in self.config.irc["permissions"]["admins"]: | |||||
| msg = "you must be a bot admin to use this command." | msg = "you must be a bot admin to use this command." | ||||
| self.connection.reply(data, msg) | self.connection.reply(data, msg) | ||||
| return | return | ||||
| @@ -25,7 +25,6 @@ import time | |||||
| from earwigbot import __version__ | from earwigbot import __version__ | ||||
| from earwigbot.commands import BaseCommand | from earwigbot.commands import BaseCommand | ||||
| from earwigbot.config import config | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Not an actual command, this module is used to respond to the CTCP | """Not an actual command, this module is used to respond to the CTCP | ||||
| @@ -63,7 +62,7 @@ class Command(BaseCommand): | |||||
| elif command == "VERSION": | elif command == "VERSION": | ||||
| default = "EarwigBot - $1 - Python/$2 https://github.com/earwig/earwigbot" | default = "EarwigBot - $1 - Python/$2 https://github.com/earwig/earwigbot" | ||||
| vers = config.irc.get("version", default) | |||||
| vers = self.config.irc.get("version", default) | |||||
| vers = vers.replace("$1", __version__) | vers = vers.replace("$1", __version__) | ||||
| vers = vers.replace("$2", platform.python_version()) | vers = vers.replace("$2", platform.python_version()) | ||||
| self.connection.notice(target, "\x01VERSION {0}\x01".format(vers)) | self.connection.notice(target, "\x01VERSION {0}\x01".format(vers)) | ||||
| @@ -25,7 +25,6 @@ import subprocess | |||||
| import re | import re | ||||
| from earwigbot.commands import BaseCommand | from earwigbot.commands import BaseCommand | ||||
| from earwigbot.config import config | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Commands to interface with the bot's git repository; use '!git' for a | """Commands to interface with the bot's git repository; use '!git' for a | ||||
| @@ -34,7 +33,7 @@ class Command(BaseCommand): | |||||
| def process(self, data): | def process(self, data): | ||||
| self.data = data | self.data = data | ||||
| if data.host not in config.irc["permissions"]["owners"]: | |||||
| if data.host not in self.config.irc["permissions"]["owners"]: | |||||
| msg = "you must be a bot owner to use this command." | msg = "you must be a bot owner to use this command." | ||||
| self.connection.reply(data, msg) | self.connection.reply(data, msg) | ||||
| return | return | ||||
| @@ -21,7 +21,6 @@ | |||||
| # SOFTWARE. | # SOFTWARE. | ||||
| from earwigbot.commands import BaseCommand | from earwigbot.commands import BaseCommand | ||||
| from earwigbot.config import config | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Restart the bot. Only the owner can do this.""" | """Restart the bot. Only the owner can do this.""" | ||||
| @@ -32,7 +31,7 @@ class Command(BaseCommand): | |||||
| return data.is_command and data.command in commands | return data.is_command and data.command in commands | ||||
| def process(self, data): | def process(self, data): | ||||
| if data.host not in config.irc["permissions"]["owners"]: | |||||
| if data.host not in self.config.irc["permissions"]["owners"]: | |||||
| msg = "you must be a bot owner to use this command." | msg = "you must be a bot owner to use this command." | ||||
| self.connection.reply(data, msg) | self.connection.reply(data, msg) | ||||
| return | return | ||||
| @@ -24,7 +24,6 @@ import threading | |||||
| import re | import re | ||||
| from earwigbot.commands import BaseCommand | from earwigbot.commands import BaseCommand | ||||
| from earwigbot.config import config | |||||
| from earwigbot.irc import KwargParseException | from earwigbot.irc import KwargParseException | ||||
| from earwigbot.tasks import task_manager | from earwigbot.tasks import task_manager | ||||
| @@ -40,7 +39,7 @@ class Command(BaseCommand): | |||||
| def process(self, data): | def process(self, data): | ||||
| self.data = data | self.data = data | ||||
| if data.host not in config.irc["permissions"]["owners"]: | |||||
| if data.host not in self.config.irc["permissions"]["owners"]: | |||||
| msg = "you must be a bot owner to use this command." | msg = "you must be a bot owner to use this command." | ||||
| self.connection.reply(data, msg) | self.connection.reply(data, msg) | ||||
| return | return | ||||
| @@ -80,7 +79,7 @@ class Command(BaseCommand): | |||||
| if tname == "MainThread": | if tname == "MainThread": | ||||
| t = "\x0302MainThread\x0301 (id {1})" | t = "\x0302MainThread\x0301 (id {1})" | ||||
| normal_threads.append(t.format(thread.ident)) | normal_threads.append(t.format(thread.ident)) | ||||
| elif tname in config.components: | |||||
| elif tname in self.config.components: | |||||
| t = "\x0302{0}\x0301 (id {1})" | t = "\x0302{0}\x0301 (id {1})" | ||||
| normal_threads.append(t.format(tname, thread.ident)) | normal_threads.append(t.format(tname, thread.ident)) | ||||
| elif tname.startswith("reminder"): | elif tname.startswith("reminder"): | ||||
| @@ -50,6 +50,7 @@ class BaseTask(object): | |||||
| (or if you do, remember super(Task, self).__init()) - use setup(). | (or if you do, remember super(Task, self).__init()) - use setup(). | ||||
| """ | """ | ||||
| self.bot = bot | self.bot = bot | ||||
| self.config = bot.config | |||||
| self.logger = bot.tasks.logger.getLogger(self.name) | self.logger = bot.tasks.logger.getLogger(self.name) | ||||
| self.setup() | self.setup() | ||||
| @@ -27,7 +27,6 @@ from threading import Lock | |||||
| import oursql | import oursql | ||||
| from earwigbot import wiki | from earwigbot import wiki | ||||
| from earwigbot.config import config | |||||
| from earwigbot.tasks import BaseTask | from earwigbot.tasks import BaseTask | ||||
| class Task(BaseTask): | class Task(BaseTask): | ||||
| @@ -37,7 +36,7 @@ class Task(BaseTask): | |||||
| number = 1 | number = 1 | ||||
| def setup(self): | def setup(self): | ||||
| cfg = config.tasks.get(self.name, {}) | |||||
| cfg = self.config.tasks.get(self.name, {}) | |||||
| self.template = cfg.get("template", "AfC suspected copyvio") | self.template = cfg.get("template", "AfC suspected copyvio") | ||||
| self.ignore_list = cfg.get("ignoreList", []) | self.ignore_list = cfg.get("ignoreList", []) | ||||
| self.min_confidence = cfg.get("minConfidence", 0.5) | self.min_confidence = cfg.get("minConfidence", 0.5) | ||||
| @@ -32,7 +32,6 @@ from numpy import arange | |||||
| import oursql | import oursql | ||||
| from earwigbot import wiki | from earwigbot import wiki | ||||
| from earwigbot.config import config | |||||
| from earwigbot.tasks import BaseTask | from earwigbot.tasks import BaseTask | ||||
| # Valid submission statuses: | # Valid submission statuses: | ||||
| @@ -58,7 +57,7 @@ class Task(BaseTask): | |||||
| name = "afc_history" | name = "afc_history" | ||||
| def setup(self): | def setup(self): | ||||
| cfg = config.tasks.get(self.name, {}) | |||||
| cfg = self.config.tasks.get(self.name, {}) | |||||
| self.num_days = cfg.get("days", 90) | self.num_days = cfg.get("days", 90) | ||||
| self.categories = cfg.get("categories", {}) | self.categories = cfg.get("categories", {}) | ||||
| @@ -30,7 +30,6 @@ from time import sleep | |||||
| import oursql | import oursql | ||||
| from earwigbot import wiki | from earwigbot import wiki | ||||
| from earwigbot.config import config | |||||
| from earwigbot.tasks import BaseTask | from earwigbot.tasks import BaseTask | ||||
| # Chart status number constants: | # Chart status number constants: | ||||
| @@ -54,7 +53,7 @@ class Task(BaseTask): | |||||
| number = 2 | number = 2 | ||||
| def setup(self): | def setup(self): | ||||
| self.cfg = cfg = config.tasks.get(self.name, {}) | |||||
| self.cfg = cfg = self.config.tasks.get(self.name, {}) | |||||
| # Set some wiki-related attributes: | # Set some wiki-related attributes: | ||||
| self.pagename = cfg.get("page", "Template:AFC statistics") | self.pagename = cfg.get("page", "Template:AFC statistics") | ||||
| @@ -27,7 +27,11 @@ This is a collection of classes and functions to read from and write to | |||||
| Wikipedia and other wiki sites. No connection whatsoever to python-wikitools | Wikipedia and other wiki sites. No connection whatsoever to python-wikitools | ||||
| written by Mr.Z-man, other than a similar purpose. We share no code. | written by Mr.Z-man, other than a similar purpose. We share no code. | ||||
| Import the toolset with `from earwigbot import wiki`. | |||||
| Import the toolset directly with `from earwigbot import wiki`. If using the | |||||
| built-in integration with the rest of the bot, that's usually not necessary: | |||||
| Bot() objects contain a `wiki` attribute containing a SitesDBManager object | |||||
| tied to the sites.db file located in the same directory as config.yml. That | |||||
| object has the principal methods get_site, add_site, and remove_site. | |||||
| """ | """ | ||||
| import logging as _log | import logging as _log | ||||
| @@ -40,5 +44,5 @@ from earwigbot.wiki.exceptions import * | |||||
| from earwigbot.wiki.category import Category | from earwigbot.wiki.category import Category | ||||
| from earwigbot.wiki.page import Page | from earwigbot.wiki.page import Page | ||||
| from earwigbot.wiki.site import Site | from earwigbot.wiki.site import Site | ||||
| from earwigbot.wiki.sitesdb import get_site, add_site, remove_site | |||||
| from earwigbot.wiki.sitesdb import SitesDBManager | |||||
| from earwigbot.wiki.user import User | from earwigbot.wiki.user import User | ||||
| @@ -29,11 +29,10 @@ import stat | |||||
| import sqlite3 as sqlite | import sqlite3 as sqlite | ||||
| from earwigbot import __version__ | from earwigbot import __version__ | ||||
| from earwigbot.config import config | |||||
| from earwigbot.wiki.exceptions import SiteNotFoundError | from earwigbot.wiki.exceptions import SiteNotFoundError | ||||
| from earwigbot.wiki.site import Site | from earwigbot.wiki.site import Site | ||||
| __all__ = ["SitesDBManager", "get_site", "add_site", "remove_site"] | |||||
| __all__ = ["SitesDBManager"] | |||||
| class SitesDBManager(object): | class SitesDBManager(object): | ||||
| """ | """ | ||||
| @@ -47,31 +46,19 @@ class SitesDBManager(object): | |||||
| remove_site -- removes a site from the database, given its name | remove_site -- removes a site from the database, given its name | ||||
| There's usually no need to use this class directly. All public methods | There's usually no need to use this class directly. All public methods | ||||
| here are available as earwigbot.wiki.get_site(), earwigbot.wiki.add_site(), | |||||
| and earwigbot.wiki.remove_site(), which use a sites.db file located in the | |||||
| same directory as our config.yml file. Lower-level access can be achieved | |||||
| here are available as bot.wiki.get_site(), bot.wiki.add_site(), and | |||||
| bot.wiki.remove_site(), which use a sites.db file located in the same | |||||
| directory as our config.yml file. Lower-level access can be achieved | |||||
| by importing the manager class | by importing the manager class | ||||
| (`from earwigbot.wiki.sitesdb import SitesDBManager`). | |||||
| (`from earwigbot.wiki import SitesDBManager`). | |||||
| """ | """ | ||||
| def __init__(self, db_file): | |||||
| """Set up the manager with an attribute for the sitesdb filename.""" | |||||
| def __init__(self, config): | |||||
| """Set up the manager with an attribute for the BotConfig object.""" | |||||
| self.config = config | |||||
| self._sitesdb = path.join(config.root_dir, "sitesdb") | |||||
| self._cookie_file = path.join(config.root_dir, ".cookies") | |||||
| self._cookiejar = None | self._cookiejar = None | ||||
| self._sitesdb = db_file | |||||
| def _load_config(self): | |||||
| """Load the bot's config. | |||||
| Called by a config-requiring function, such as get_site(), when config | |||||
| has not been loaded. This will usually happen only if we're running | |||||
| code directly from Python's interpreter and not the bot itself, because | |||||
| bot.py and earwigbot.runner will already call these functions. | |||||
| """ | |||||
| is_encrypted = config.load() | |||||
| if is_encrypted: # Passwords in the config file are encrypted | |||||
| key = getpass("Enter key to unencrypt bot passwords: ") | |||||
| config._decryption_key = key | |||||
| config.decrypt(config.wiki, "password") | |||||
| def _get_cookiejar(self): | def _get_cookiejar(self): | ||||
| """Return a LWPCookieJar object loaded from our .cookies file. | """Return a LWPCookieJar object loaded from our .cookies file. | ||||
| @@ -89,8 +76,7 @@ class SitesDBManager(object): | |||||
| if self._cookiejar: | if self._cookiejar: | ||||
| return self._cookiejar | return self._cookiejar | ||||
| cookie_file = path.join(config.root_dir, ".cookies") | |||||
| self._cookiejar = LWPCookieJar(cookie_file) | |||||
| self._cookiejar = LWPCookieJar(self._cookie_file) | |||||
| try: | try: | ||||
| self._cookiejar.load() | self._cookiejar.load() | ||||
| @@ -163,10 +149,12 @@ class SitesDBManager(object): | |||||
| This calls _load_site_from_sitesdb(), so SiteNotFoundError will be | This calls _load_site_from_sitesdb(), so SiteNotFoundError will be | ||||
| raised if the site is not in our sitesdb. | raised if the site is not in our sitesdb. | ||||
| """ | """ | ||||
| cookiejar = self._get_cookiejar() | |||||
| (name, project, lang, base_url, article_path, script_path, sql, | (name, project, lang, base_url, article_path, script_path, sql, | ||||
| namespaces) = self._load_site_from_sitesdb(name) | namespaces) = self._load_site_from_sitesdb(name) | ||||
| config = self.config | |||||
| login = (config.wiki.get("username"), config.wiki.get("password")) | login = (config.wiki.get("username"), config.wiki.get("password")) | ||||
| cookiejar = self._get_cookiejar() | |||||
| user_agent = config.wiki.get("userAgent") | user_agent = config.wiki.get("userAgent") | ||||
| use_https = config.wiki.get("useHTTPS", False) | use_https = config.wiki.get("useHTTPS", False) | ||||
| assert_edit = config.wiki.get("assert") | assert_edit = config.wiki.get("assert") | ||||
| @@ -265,9 +253,6 @@ class SitesDBManager(object): | |||||
| cannot be found in the sitesdb, SiteNotFoundError will be raised. An | cannot be found in the sitesdb, SiteNotFoundError will be raised. An | ||||
| empty sitesdb will be created if none is found. | empty sitesdb will be created if none is found. | ||||
| """ | """ | ||||
| if not config.is_loaded(): | |||||
| self._load_config() | |||||
| # Someone specified a project without a lang, or vice versa: | # Someone specified a project without a lang, or vice versa: | ||||
| if (project and not lang) or (not project and lang): | if (project and not lang) or (not project and lang): | ||||
| e = "Keyword arguments 'lang' and 'project' must be specified together." | e = "Keyword arguments 'lang' and 'project' must be specified together." | ||||
| @@ -276,7 +261,7 @@ class SitesDBManager(object): | |||||
| # No args given, so return our default site: | # No args given, so return our default site: | ||||
| if not name and not project and not lang: | if not name and not project and not lang: | ||||
| try: | try: | ||||
| default = config.wiki["defaultSite"] | |||||
| default = self.config.wiki["defaultSite"] | |||||
| except KeyError: | except KeyError: | ||||
| e = "Default site is not specified in config." | e = "Default site is not specified in config." | ||||
| raise SiteNotFoundError(e) | raise SiteNotFoundError(e) | ||||
| @@ -322,17 +307,15 @@ class SitesDBManager(object): | |||||
| site info). Raises SiteNotFoundError if not enough information has | site info). Raises SiteNotFoundError if not enough information has | ||||
| been provided to identify the site (e.g. a project but not a lang). | been provided to identify the site (e.g. a project but not a lang). | ||||
| """ | """ | ||||
| if not config.is_loaded(): | |||||
| self._load_config() | |||||
| if not base_url: | if not base_url: | ||||
| if not project or not lang: | if not project or not lang: | ||||
| e = "Without a base_url, both a project and a lang must be given." | e = "Without a base_url, both a project and a lang must be given." | ||||
| raise SiteNotFoundError(e) | raise SiteNotFoundError(e) | ||||
| base_url = "//{0}.{1}.org".format(lang, project) | base_url = "//{0}.{1}.org".format(lang, project) | ||||
| cookiejar = self._get_cookiejar() | |||||
| config = self.config | |||||
| login = (config.wiki.get("username"), config.wiki.get("password")) | login = (config.wiki.get("username"), config.wiki.get("password")) | ||||
| cookiejar = self._get_cookiejar() | |||||
| user_agent = config.wiki.get("userAgent") | user_agent = config.wiki.get("userAgent") | ||||
| use_https = config.wiki.get("useHTTPS", False) | use_https = config.wiki.get("useHTTPS", False) | ||||
| assert_edit = config.wiki.get("assert") | assert_edit = config.wiki.get("assert") | ||||
| @@ -358,9 +341,6 @@ class SitesDBManager(object): | |||||
| was given but not a language, or vice versa. Will create an empty | was given but not a language, or vice versa. Will create an empty | ||||
| sitesdb if none was found. | sitesdb if none was found. | ||||
| """ | """ | ||||
| if not config.is_loaded(): | |||||
| self._load_config() | |||||
| # Someone specified a project without a lang, or vice versa: | # Someone specified a project without a lang, or vice versa: | ||||
| if (project and not lang) or (not project and lang): | if (project and not lang) or (not project and lang): | ||||
| e = "Keyword arguments 'lang' and 'project' must be specified together." | e = "Keyword arguments 'lang' and 'project' must be specified together." | ||||
| @@ -381,12 +361,3 @@ class SitesDBManager(object): | |||||
| return self._remove_site_from_sitesdb(name) | return self._remove_site_from_sitesdb(name) | ||||
| return False | return False | ||||
| _root = path.split(path.split(path.dirname(path.abspath(__file__)))[0])[0] | |||||
| _dbfile = path.join(_root, "sites.db") | |||||
| _manager = SitesDBManager(_dbfile) | |||||
| del _root, _dbfile | |||||
| get_site = _manager.get_site | |||||
| add_site = _manager.add_site | |||||
| remove_site = _manager.remove_site | |||||