@@ -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 |