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