diff --git a/earwigbot/bot.py b/earwigbot/bot.py index 42f3b89..a005ac6 100644 --- a/earwigbot/bot.py +++ b/earwigbot/bot.py @@ -27,6 +27,7 @@ from earwigbot.commands import CommandManager from earwigbot.config import BotConfig from earwigbot.irc import Frontend, Watcher from earwigbot.tasks import TaskManager +from earwigbot.wiki import SitesDBManager __all__ = ["Bot"] @@ -51,6 +52,7 @@ class Bot(object): self.logger = logging.getLogger("earwigbot") self.commands = CommandManager(self) self.tasks = TaskManager(self) + self.wiki = SitesDBManager(self.config) self.frontend = None self.watcher = None @@ -100,12 +102,13 @@ class Bot(object): sleep(5) 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.tasks.load() self._start_irc_components() @@ -121,6 +124,7 @@ class Bot(object): self._start_irc_components() def stop(self): - self._stop_irc_components() + with self.component_lock: + self._stop_irc_components() self._keep_looping = False sleep(3) # Give a few seconds to finish closing IRC connections diff --git a/earwigbot/commands/__init__.py b/earwigbot/commands/__init__.py index a22fcf6..ba09eac 100644 --- a/earwigbot/commands/__init__.py +++ b/earwigbot/commands/__init__.py @@ -58,9 +58,10 @@ class BaseCommand(object): super(Command, self).__init__() first. """ self.bot = bot + self.config = bot.config 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.""" self.connection = self.bot.frontend self.process(data) @@ -158,7 +159,7 @@ class CommandManager(object): if hook in command.hooks: if command.check(data): try: - command._execute(data) + command._wrap_process(data) except Exception: e = "Error executing command '{0}':" self.logger.exception(e.format(data.command)) diff --git a/earwigbot/commands/afc_status.py b/earwigbot/commands/afc_status.py index a475caf..f03bf98 100644 --- a/earwigbot/commands/afc_status.py +++ b/earwigbot/commands/afc_status.py @@ -24,7 +24,6 @@ import re from earwigbot import wiki from earwigbot.commands import BaseCommand -from earwigbot.config import config class Command(BaseCommand): """Get the number of pending AfC submissions, open redirect requests, and @@ -39,7 +38,7 @@ class Command(BaseCommand): try: 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 except IndexError: pass diff --git a/earwigbot/commands/chanops.py b/earwigbot/commands/chanops.py index dd59353..1bec2d6 100644 --- a/earwigbot/commands/chanops.py +++ b/earwigbot/commands/chanops.py @@ -21,7 +21,6 @@ # SOFTWARE. from earwigbot.commands import BaseCommand -from earwigbot.config import config class Command(BaseCommand): """Voice, devoice, op, or deop users in the channel.""" @@ -39,7 +38,7 @@ class Command(BaseCommand): self.connection.reply(data, msg) 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." self.connection.reply(data, msg) return diff --git a/earwigbot/commands/ctcp.py b/earwigbot/commands/ctcp.py index 80b56e2..6641d7e 100644 --- a/earwigbot/commands/ctcp.py +++ b/earwigbot/commands/ctcp.py @@ -25,7 +25,6 @@ import time from earwigbot import __version__ from earwigbot.commands import BaseCommand -from earwigbot.config import config class Command(BaseCommand): """Not an actual command, this module is used to respond to the CTCP @@ -63,7 +62,7 @@ class Command(BaseCommand): elif command == "VERSION": 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("$2", platform.python_version()) self.connection.notice(target, "\x01VERSION {0}\x01".format(vers)) diff --git a/earwigbot/commands/git.py b/earwigbot/commands/git.py index dfd9aba..c86ff70 100644 --- a/earwigbot/commands/git.py +++ b/earwigbot/commands/git.py @@ -25,7 +25,6 @@ import subprocess import re from earwigbot.commands import BaseCommand -from earwigbot.config import config class Command(BaseCommand): """Commands to interface with the bot's git repository; use '!git' for a @@ -34,7 +33,7 @@ class Command(BaseCommand): def process(self, 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." self.connection.reply(data, msg) return diff --git a/earwigbot/commands/restart.py b/earwigbot/commands/restart.py index 7456e4b..8756fe1 100644 --- a/earwigbot/commands/restart.py +++ b/earwigbot/commands/restart.py @@ -21,7 +21,6 @@ # SOFTWARE. from earwigbot.commands import BaseCommand -from earwigbot.config import config class Command(BaseCommand): """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 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." self.connection.reply(data, msg) return diff --git a/earwigbot/commands/threads.py b/earwigbot/commands/threads.py index 7cf70ae..976eb71 100644 --- a/earwigbot/commands/threads.py +++ b/earwigbot/commands/threads.py @@ -24,7 +24,6 @@ import threading import re from earwigbot.commands import BaseCommand -from earwigbot.config import config from earwigbot.irc import KwargParseException from earwigbot.tasks import task_manager @@ -40,7 +39,7 @@ class Command(BaseCommand): def process(self, 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." self.connection.reply(data, msg) return @@ -80,7 +79,7 @@ class Command(BaseCommand): if tname == "MainThread": t = "\x0302MainThread\x0301 (id {1})" normal_threads.append(t.format(thread.ident)) - elif tname in config.components: + elif tname in self.config.components: t = "\x0302{0}\x0301 (id {1})" normal_threads.append(t.format(tname, thread.ident)) elif tname.startswith("reminder"): diff --git a/earwigbot/tasks/__init__.py b/earwigbot/tasks/__init__.py index dcd963c..7c268f9 100644 --- a/earwigbot/tasks/__init__.py +++ b/earwigbot/tasks/__init__.py @@ -50,6 +50,7 @@ class BaseTask(object): (or if you do, remember super(Task, self).__init()) - use setup(). """ self.bot = bot + self.config = bot.config self.logger = bot.tasks.logger.getLogger(self.name) self.setup() diff --git a/earwigbot/tasks/afc_copyvios.py b/earwigbot/tasks/afc_copyvios.py index 0956b43..552654c 100644 --- a/earwigbot/tasks/afc_copyvios.py +++ b/earwigbot/tasks/afc_copyvios.py @@ -27,7 +27,6 @@ from threading import Lock import oursql from earwigbot import wiki -from earwigbot.config import config from earwigbot.tasks import BaseTask class Task(BaseTask): @@ -37,7 +36,7 @@ class Task(BaseTask): number = 1 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.ignore_list = cfg.get("ignoreList", []) self.min_confidence = cfg.get("minConfidence", 0.5) diff --git a/earwigbot/tasks/afc_history.py b/earwigbot/tasks/afc_history.py index 9e146f9..e59a5c0 100644 --- a/earwigbot/tasks/afc_history.py +++ b/earwigbot/tasks/afc_history.py @@ -32,7 +32,6 @@ from numpy import arange import oursql from earwigbot import wiki -from earwigbot.config import config from earwigbot.tasks import BaseTask # Valid submission statuses: @@ -58,7 +57,7 @@ class Task(BaseTask): name = "afc_history" def setup(self): - cfg = config.tasks.get(self.name, {}) + cfg = self.config.tasks.get(self.name, {}) self.num_days = cfg.get("days", 90) self.categories = cfg.get("categories", {}) diff --git a/earwigbot/tasks/afc_statistics.py b/earwigbot/tasks/afc_statistics.py index 79070ae..0fcbec4 100644 --- a/earwigbot/tasks/afc_statistics.py +++ b/earwigbot/tasks/afc_statistics.py @@ -30,7 +30,6 @@ from time import sleep import oursql from earwigbot import wiki -from earwigbot.config import config from earwigbot.tasks import BaseTask # Chart status number constants: @@ -54,7 +53,7 @@ class Task(BaseTask): number = 2 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: self.pagename = cfg.get("page", "Template:AFC statistics") diff --git a/earwigbot/wiki/__init__.py b/earwigbot/wiki/__init__.py index e48be82..00b5dd4 100644 --- a/earwigbot/wiki/__init__.py +++ b/earwigbot/wiki/__init__.py @@ -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 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 @@ -40,5 +44,5 @@ from earwigbot.wiki.exceptions import * from earwigbot.wiki.category import Category from earwigbot.wiki.page import Page 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 diff --git a/earwigbot/wiki/sitesdb.py b/earwigbot/wiki/sitesdb.py index 0bd5c76..a5c9fe7 100644 --- a/earwigbot/wiki/sitesdb.py +++ b/earwigbot/wiki/sitesdb.py @@ -29,11 +29,10 @@ import stat import sqlite3 as sqlite from earwigbot import __version__ -from earwigbot.config import config from earwigbot.wiki.exceptions import SiteNotFoundError from earwigbot.wiki.site import Site -__all__ = ["SitesDBManager", "get_site", "add_site", "remove_site"] +__all__ = ["SitesDBManager"] class SitesDBManager(object): """ @@ -47,31 +46,19 @@ class SitesDBManager(object): remove_site -- removes a site from the database, given its name 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 - (`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._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): """Return a LWPCookieJar object loaded from our .cookies file. @@ -89,8 +76,7 @@ class SitesDBManager(object): if 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: self._cookiejar.load() @@ -163,10 +149,12 @@ class SitesDBManager(object): This calls _load_site_from_sitesdb(), so SiteNotFoundError will be raised if the site is not in our sitesdb. """ + cookiejar = self._get_cookiejar() (name, project, lang, base_url, article_path, script_path, sql, namespaces) = self._load_site_from_sitesdb(name) + + config = self.config login = (config.wiki.get("username"), config.wiki.get("password")) - cookiejar = self._get_cookiejar() user_agent = config.wiki.get("userAgent") use_https = config.wiki.get("useHTTPS", False) assert_edit = config.wiki.get("assert") @@ -265,9 +253,6 @@ class SitesDBManager(object): cannot be found in the sitesdb, SiteNotFoundError will be raised. An 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: if (project and not lang) or (not project and lang): 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: if not name and not project and not lang: try: - default = config.wiki["defaultSite"] + default = self.config.wiki["defaultSite"] except KeyError: e = "Default site is not specified in config." raise SiteNotFoundError(e) @@ -322,17 +307,15 @@ class SitesDBManager(object): site info). Raises SiteNotFoundError if not enough information has 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 project or not lang: e = "Without a base_url, both a project and a lang must be given." raise SiteNotFoundError(e) base_url = "//{0}.{1}.org".format(lang, project) + cookiejar = self._get_cookiejar() + config = self.config login = (config.wiki.get("username"), config.wiki.get("password")) - cookiejar = self._get_cookiejar() user_agent = config.wiki.get("userAgent") use_https = config.wiki.get("useHTTPS", False) 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 sitesdb if none was found. """ - if not config.is_loaded(): - self._load_config() - # Someone specified a project without a lang, or vice versa: if (project and not lang) or (not project and lang): 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 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