| @@ -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 | |||
| @@ -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)) | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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)) | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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"): | |||
| @@ -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() | |||
| @@ -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) | |||
| @@ -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", {}) | |||
| @@ -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") | |||
| @@ -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 | |||
| @@ -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 | |||