@@ -0,0 +1,141 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2012 Ben Kurtovic <ben.kurtovic@verizon.net> | |||||
# | |||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
# of this software and associated documentation files (the "Software"), to deal | |||||
# in the Software without restriction, including without limitation the rights | |||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
# copies of the Software, and to permit persons to whom the Software is | |||||
# furnished to do so, subject to the following conditions: | |||||
# | |||||
# The above copyright notice and this permission notice shall be included in | |||||
# all copies or substantial portions of the Software. | |||||
# | |||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
# SOFTWARE. | |||||
import re | |||||
from earwigbot.commands import Command | |||||
class Access(Command): | |||||
"""Control and get info on who can access the bot.""" | |||||
name = "access" | |||||
commands = ["access", "permission", "permissions", "perm", "perms"] | |||||
def process(self, data): | |||||
if not data.args: | |||||
self.reply(data, "Subcommands are self, list, add, remove.") | |||||
return | |||||
db = self.config.irc["permissions"] | |||||
if data.args[0] == "self": | |||||
self.do_self(data, db) | |||||
elif data.args[0] == "list": | |||||
self.do_list(data, db) | |||||
elif data.args[0] == "add": | |||||
self.do_add(data, db) | |||||
elif data.args[0] == "remove": | |||||
self.do_remove(data, db) | |||||
else: | |||||
msg = "Unknown subcommand \x0303{0}\x0F.".format(data.args[0]) | |||||
self.reply(data, msg) | |||||
def do_self(self, data, db): | |||||
if db.is_owner(data): | |||||
msg = "You are a bot owner (matching rule \x0302{0}\x0F)." | |||||
self.reply(data, msg.format(db.is_owner(data))) | |||||
elif db.is_admin(data): | |||||
msg = "You are a bot admin (matching rule \x0302{0}\x0F)." | |||||
self.reply(data, msg.format(db.is_admin(data))) | |||||
else: | |||||
self.reply(data, "You do not match any bot access rules.") | |||||
def do_list(self, data, db): | |||||
if len(data.args) > 1: | |||||
if data.args[1] in ["owner", "owners"]: | |||||
name, rules = "owners", db.data.get(db.OWNERS) | |||||
elif data.args[1] in ["admin", "admins"]: | |||||
name, rules = "admins", db.data.get(db.ADMINS) | |||||
else: | |||||
msg = "Unknown access level \x0302{0}\x0F." | |||||
self.reply(data, msg.format(data.args[1])) | |||||
return | |||||
if rules: | |||||
msg = "Bot {0}: {1}.".format(name, ", ".join(map(str, rules))) | |||||
else: | |||||
msg = "No bot {0}.".format(name) | |||||
self.reply(data, msg) | |||||
else: | |||||
owners = len(db.data.get(db.OWNERS, [])) | |||||
admins = len(db.data.get(db.ADMINS, [])) | |||||
msg = "There are {0} bot owners and {1} bot admins. Use '!{2} list owners' or '!{2} list admins' for details." | |||||
self.reply(data, msg.format(owners, admins, data.command)) | |||||
def do_add(self, data, db): | |||||
user = self.get_user_from_args(data) | |||||
if user: | |||||
nick, ident, host = user | |||||
if data.args[1] in ["owner", "owners"]: | |||||
name, level, adder = "owner", db.OWNER, db.add_owner | |||||
else: | |||||
name, level, adder = "admin", db.ADMIN, db.add_admin | |||||
if db.has_exact(nick, ident, host, level): | |||||
rule = "{0}!{1}@{2}".format(nick, ident, host) | |||||
msg = "\x0302{0}\x0F is already a bot {1}.".format(rule, name) | |||||
self.reply(data, msg) | |||||
else: | |||||
rule = adder(nick, ident, host) | |||||
msg = "Added bot {0} \x0302{1}\x0F.".format(name, rule) | |||||
self.reply(data, msg) | |||||
def do_remove(self, data, db): | |||||
user = self.get_user_from_args(data) | |||||
if user: | |||||
nick, ident, host = user | |||||
if data.args[1] in ["owner", "owners"]: | |||||
name, level, rmver = "owner", db.OWNER, db.remove_owner | |||||
else: | |||||
name, level, rmver = "admin", db.ADMIN, db.remove_admin | |||||
rule = rmver(nick, ident, host) | |||||
if rule: | |||||
msg = "Removed bot {0} \x0302{1}\x0F.".format(name, rule) | |||||
self.reply(data, msg) | |||||
else: | |||||
rule = "{0}!{1}@{2}".format(nick, ident, host) | |||||
msg = "No bot {0} matching \x0302{1}\x0F.".format(name, rule) | |||||
self.reply(data, msg) | |||||
def get_user_from_args(self, data): | |||||
if not db.is_owner(data): | |||||
msg = "You must be a bot owner to add users to the access list." | |||||
self.reply(data, msg) | |||||
return | |||||
levels = ["owner", "owners", "admin", "admins"] | |||||
if len(data.args) == 1 or data.args[1] not in levels: | |||||
msg = "Please specify an access level ('owners' or 'admins')." | |||||
self.reply(data, msg) | |||||
return | |||||
if len(data.args) == 2: | |||||
self.no_arg_error(data) | |||||
return | |||||
if "nick" in data.kwargs or "ident" in kwargs or "host" in kwargs: | |||||
nick = data.kwargs.get("nick", "*") | |||||
ident = data.kwargs.get("ident", "*") | |||||
host = data.kwargs.get("host", "*") | |||||
return nick, ident, host | |||||
user = re.match(r"(.*?)!(.*?)@(.*?)$", data.args[2]) | |||||
if not user: | |||||
self.no_arg_error(data) | |||||
return | |||||
return user.group(1), user.group(2), user.group(3) | |||||
def no_arg_error(self, data): | |||||
msg = 'Please specify a user, either as "\x0302nick\x0F!\x0302ident\x0F@\x0302host\x0F"' | |||||
msg += ' or "nick=\x0302nick\x0F, ident=\x0302ident\x0F, host=\x0302host\x0F".' | |||||
self.reply(data, msg) |
@@ -36,7 +36,7 @@ class ChanOps(Command): | |||||
de_escalate = data.command in ["devoice", "deop"] | de_escalate = data.command in ["devoice", "deop"] | ||||
if de_escalate and (not data.args or data.args[0] == data.nick): | if de_escalate and (not data.args or data.args[0] == data.nick): | ||||
target = data.nick | target = data.nick | ||||
elif data.host not in self.config.irc["permissions"]["admins"]: | |||||
elif not self.config.irc["permissions"].is_admin(data): | |||||
self.reply(data, "You must be a bot admin to use this command.") | self.reply(data, "You must be a bot admin to use this command.") | ||||
return | return | ||||
@@ -39,7 +39,7 @@ class Git(Command): | |||||
def process(self, data): | def process(self, data): | ||||
self.data = data | self.data = data | ||||
if data.host not in self.config.irc["permissions"]["owners"]: | |||||
if not self.config.irc["permissions"].is_owner(data): | |||||
msg = "You must be a bot owner to use this command." | msg = "You must be a bot owner to use this command." | ||||
self.reply(data, msg) | self.reply(data, msg) | ||||
return | return | ||||
@@ -78,7 +78,7 @@ class Git(Command): | |||||
elif command == "status": | elif command == "status": | ||||
self.do_status() | self.do_status() | ||||
else: # They asked us to do something we don't know | else: # They asked us to do something we don't know | ||||
msg = "Ynknown argument: \x0303{0}\x0F.".format(data.args[0]) | |||||
msg = "Unknown argument: \x0303{0}\x0F.".format(data.args[0]) | |||||
self.reply(data, msg) | self.reply(data, msg) | ||||
def get_repos(self): | def get_repos(self): | ||||
@@ -29,7 +29,7 @@ class Quit(Command): | |||||
commands = ["quit", "restart", "reload"] | commands = ["quit", "restart", "reload"] | ||||
def process(self, data): | def process(self, data): | ||||
if data.host not in self.config.irc["permissions"]["owners"]: | |||||
if not self.config.irc["permissions"].is_owner(data): | |||||
self.reply(data, "You must be a bot owner to use this command.") | self.reply(data, "You must be a bot owner to use this command.") | ||||
return | return | ||||
if data.command == "quit": | if data.command == "quit": | ||||
@@ -32,7 +32,7 @@ class Threads(Command): | |||||
def process(self, data): | def process(self, data): | ||||
self.data = data | self.data = data | ||||
if data.host not in self.config.irc["permissions"]["owners"]: | |||||
if not self.config.irc["permissions"].is_owner(data): | |||||
msg = "You must be a bot owner to use this command." | msg = "You must be a bot owner to use this command." | ||||
self.reply(data, msg) | self.reply(data, msg) | ||||
return | return | ||||
@@ -41,6 +41,9 @@ try: | |||||
except ImportError: | except ImportError: | ||||
yaml = None | yaml = None | ||||
from earwigbot.config.formatter import BotFormatter | |||||
from earwigbot.config.node import ConfigNode | |||||
from earwigbot.config.permissions import PermissionsDB | |||||
from earwigbot.exceptions import NoConfigError | from earwigbot.exceptions import NoConfigError | ||||
__all__ = ["BotConfig"] | __all__ = ["BotConfig"] | ||||
@@ -75,17 +78,19 @@ class BotConfig(object): | |||||
def __init__(self, root_dir, level): | def __init__(self, root_dir, level): | ||||
self._root_dir = root_dir | self._root_dir = root_dir | ||||
self._logging_level = level | self._logging_level = level | ||||
self._config_path = path.join(self._root_dir, "config.yml") | |||||
self._log_dir = path.join(self._root_dir, "logs") | |||||
self._config_path = path.join(self.root_dir, "config.yml") | |||||
self._log_dir = path.join(self.root_dir, "logs") | |||||
perms_file = path.join(self.root_dir, "permissions.db") | |||||
self._permissions = PermissionsDB(perms_file) | |||||
self._decryption_cipher = None | self._decryption_cipher = None | ||||
self._data = None | self._data = None | ||||
self._components = _ConfigNode() | |||||
self._wiki = _ConfigNode() | |||||
self._irc = _ConfigNode() | |||||
self._commands = _ConfigNode() | |||||
self._tasks = _ConfigNode() | |||||
self._metadata = _ConfigNode() | |||||
self._components = ConfigNode() | |||||
self._wiki = ConfigNode() | |||||
self._irc = ConfigNode() | |||||
self._commands = ConfigNode() | |||||
self._tasks = ConfigNode() | |||||
self._metadata = ConfigNode() | |||||
self._nodes = [self._components, self._wiki, self._irc, self._commands, | self._nodes = [self._components, self._wiki, self._irc, self._commands, | ||||
self._tasks, self._metadata] | self._tasks, self._metadata] | ||||
@@ -123,8 +128,8 @@ class BotConfig(object): | |||||
logger = logging.getLogger("earwigbot") | logger = logging.getLogger("earwigbot") | ||||
logger.handlers = [] # Remove any handlers already attached to us | logger.handlers = [] # Remove any handlers already attached to us | ||||
logger.setLevel(logging.DEBUG) | logger.setLevel(logging.DEBUG) | ||||
color_formatter = _BotFormatter(color=True) | |||||
formatter = _BotFormatter() | |||||
color_formatter = BotFormatter(color=True) | |||||
formatter = BotFormatter() | |||||
if self.metadata.get("enableLogging"): | if self.metadata.get("enableLogging"): | ||||
hand = logging.handlers.TimedRotatingFileHandler | hand = logging.handlers.TimedRotatingFileHandler | ||||
@@ -253,7 +258,7 @@ class BotConfig(object): | |||||
exit. | exit. | ||||
Data from the config file is stored in six | Data from the config file is stored in six | ||||
:py:class:`~earwigbot.config._ConfigNode`\ s (:py:attr:`components`, | |||||
:py:class:`~earwigbot.config.ConfigNode`\ s (:py:attr:`components`, | |||||
:py:attr:`wiki`, :py:attr:`irc`, :py:attr:`commands`, :py:attr:`tasks`, | :py:attr:`wiki`, :py:attr:`irc`, :py:attr:`commands`, :py:attr:`tasks`, | ||||
:py:attr:`metadata`) for easy access (as well as the lower-level | :py:attr:`metadata`) for easy access (as well as the lower-level | ||||
:py:attr:`data` attribute). If passwords are encrypted, we'll use | :py:attr:`data` attribute). If passwords are encrypted, we'll use | ||||
@@ -289,6 +294,10 @@ class BotConfig(object): | |||||
for node, nodes in self._decryptable_nodes: | for node, nodes in self._decryptable_nodes: | ||||
self._decrypt(node, nodes) | self._decrypt(node, nodes) | ||||
if self.irc: | |||||
self.irc["permissions"] = self._permissions | |||||
self._permissions.load() | |||||
def decrypt(self, node, *nodes): | def decrypt(self, node, *nodes): | ||||
"""Decrypt an object in our config tree. | """Decrypt an object in our config tree. | ||||
@@ -342,81 +351,3 @@ class BotConfig(object): | |||||
pass | pass | ||||
return tasks | return tasks | ||||
class _ConfigNode(object): | |||||
def __iter__(self): | |||||
for key in self.__dict__: | |||||
yield key | |||||
def __getitem__(self, item): | |||||
return self.__dict__.__getitem__(item) | |||||
def _dump(self): | |||||
data = self.__dict__.copy() | |||||
for key, val in data.iteritems(): | |||||
if isinstance(val, _ConfigNode): | |||||
data[key] = val._dump() | |||||
return data | |||||
def _load(self, data): | |||||
self.__dict__ = data.copy() | |||||
def _decrypt(self, cipher, intermediates, item): | |||||
base = self.__dict__ | |||||
for inter in intermediates: | |||||
try: | |||||
base = base[inter] | |||||
except KeyError: | |||||
return | |||||
if item in base: | |||||
ciphertext = base[item].decode("hex") | |||||
base[item] = cipher.decrypt(ciphertext).rstrip("\x00") | |||||
def get(self, *args, **kwargs): | |||||
return self.__dict__.get(*args, **kwargs) | |||||
def keys(self): | |||||
return self.__dict__.keys() | |||||
def values(self): | |||||
return self.__dict__.values() | |||||
def items(self): | |||||
return self.__dict__.items() | |||||
def iterkeys(self): | |||||
return self.__dict__.iterkeys() | |||||
def itervalues(self): | |||||
return self.__dict__.itervalues() | |||||
def iteritems(self): | |||||
return self.__dict__.iteritems() | |||||
class _BotFormatter(logging.Formatter): | |||||
def __init__(self, color=False): | |||||
self._format = super(_BotFormatter, self).format | |||||
if color: | |||||
fmt = "[%(asctime)s %(lvl)s] %(name)s: %(message)s" | |||||
self.format = lambda record: self._format(self.format_color(record)) | |||||
else: | |||||
fmt = "[%(asctime)s %(levelname)-8s] %(name)s: %(message)s" | |||||
self.format = self._format | |||||
datefmt = "%Y-%m-%d %H:%M:%S" | |||||
super(_BotFormatter, self).__init__(fmt=fmt, datefmt=datefmt) | |||||
def format_color(self, record): | |||||
l = record.levelname.ljust(8) | |||||
if record.levelno == logging.DEBUG: | |||||
record.lvl = l.join(("\x1b[34m", "\x1b[0m")) # Blue | |||||
if record.levelno == logging.INFO: | |||||
record.lvl = l.join(("\x1b[32m", "\x1b[0m")) # Green | |||||
if record.levelno == logging.WARNING: | |||||
record.lvl = l.join(("\x1b[33m", "\x1b[0m")) # Yellow | |||||
if record.levelno == logging.ERROR: | |||||
record.lvl = l.join(("\x1b[31m", "\x1b[0m")) # Red | |||||
if record.levelno == logging.CRITICAL: | |||||
record.lvl = l.join(("\x1b[1m\x1b[31m", "\x1b[0m")) # Bold red | |||||
return record |
@@ -0,0 +1,51 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2012 Ben Kurtovic <ben.kurtovic@verizon.net> | |||||
# | |||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
# of this software and associated documentation files (the "Software"), to deal | |||||
# in the Software without restriction, including without limitation the rights | |||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
# copies of the Software, and to permit persons to whom the Software is | |||||
# furnished to do so, subject to the following conditions: | |||||
# | |||||
# The above copyright notice and this permission notice shall be included in | |||||
# all copies or substantial portions of the Software. | |||||
# | |||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
# SOFTWARE. | |||||
import logging | |||||
__all__ = ["BotFormatter"] | |||||
class BotFormatter(logging.Formatter): | |||||
def __init__(self, color=False): | |||||
self._format = super(BotFormatter, self).format | |||||
if color: | |||||
fmt = "[%(asctime)s %(lvl)s] %(name)s: %(message)s" | |||||
self.format = lambda rec: self._format(self.format_color(rec)) | |||||
else: | |||||
fmt = "[%(asctime)s %(levelname)-8s] %(name)s: %(message)s" | |||||
self.format = self._format | |||||
datefmt = "%Y-%m-%d %H:%M:%S" | |||||
super(BotFormatter, self).__init__(fmt=fmt, datefmt=datefmt) | |||||
def format_color(self, record): | |||||
l = record.levelname.ljust(8) | |||||
if record.levelno == logging.DEBUG: | |||||
record.lvl = l.join(("\x1b[34m", "\x1b[0m")) # Blue | |||||
if record.levelno == logging.INFO: | |||||
record.lvl = l.join(("\x1b[32m", "\x1b[0m")) # Green | |||||
if record.levelno == logging.WARNING: | |||||
record.lvl = l.join(("\x1b[33m", "\x1b[0m")) # Yellow | |||||
if record.levelno == logging.ERROR: | |||||
record.lvl = l.join(("\x1b[31m", "\x1b[0m")) # Red | |||||
if record.levelno == logging.CRITICAL: | |||||
record.lvl = l.join(("\x1b[1m\x1b[31m", "\x1b[0m")) # Bold red | |||||
return record |
@@ -0,0 +1,97 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2012 Ben Kurtovic <ben.kurtovic@verizon.net> | |||||
# | |||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
# of this software and associated documentation files (the "Software"), to deal | |||||
# in the Software without restriction, including without limitation the rights | |||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
# copies of the Software, and to permit persons to whom the Software is | |||||
# furnished to do so, subject to the following conditions: | |||||
# | |||||
# The above copyright notice and this permission notice shall be included in | |||||
# all copies or substantial portions of the Software. | |||||
# | |||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
# SOFTWARE. | |||||
__all__ = ["ConfigNode"] | |||||
class ConfigNode(object): | |||||
def __init__(self): | |||||
self._data = {} | |||||
def __repr__(self): | |||||
return self._data | |||||
def __nonzero__(self): | |||||
return bool(self._data) | |||||
def __len__(self): | |||||
retrun len(self._data) | |||||
def __getitem__(self, key): | |||||
return self._data[key] | |||||
def __setitem__(self, key, item): | |||||
self._data[key] = item | |||||
def __getattr__(self, key): | |||||
return self._data[key] | |||||
def __setattr__(self, key, item): | |||||
self._data[key] = item | |||||
def __iter__(self): | |||||
for key in self._data: | |||||
yield key | |||||
def __contains__(self, item): | |||||
return item in self._data | |||||
def _dump(self): | |||||
data = self._data.copy() | |||||
for key, val in data.iteritems(): | |||||
if isinstance(val, ConfigNode): | |||||
data[key] = val._dump() | |||||
return data | |||||
def _load(self, data): | |||||
self._data = data.copy() | |||||
def _decrypt(self, cipher, intermediates, item): | |||||
base = self._data | |||||
for inter in intermediates: | |||||
try: | |||||
base = base[inter] | |||||
except KeyError: | |||||
return | |||||
if item in base: | |||||
ciphertext = base[item].decode("hex") | |||||
base[item] = cipher.decrypt(ciphertext).rstrip("\x00") | |||||
def get(self, *args, **kwargs): | |||||
return self._data.get(*args, **kwargs) | |||||
def keys(self): | |||||
return self._data.keys() | |||||
def values(self): | |||||
return self._data.values() | |||||
def items(self): | |||||
return self._data.items() | |||||
def iterkeys(self): | |||||
return self._data.iterkeys() | |||||
def itervalues(self): | |||||
return self._data.itervalues() | |||||
def iteritems(self): | |||||
return self.__dict__.iteritems() |
@@ -0,0 +1,174 @@ | |||||
# -*- coding: utf-8 -*- | |||||
# | |||||
# Copyright (C) 2009-2012 Ben Kurtovic <ben.kurtovic@verizon.net> | |||||
# | |||||
# Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
# of this software and associated documentation files (the "Software"), to deal | |||||
# in the Software without restriction, including without limitation the rights | |||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
# copies of the Software, and to permit persons to whom the Software is | |||||
# furnished to do so, subject to the following conditions: | |||||
# | |||||
# The above copyright notice and this permission notice shall be included in | |||||
# all copies or substantial portions of the Software. | |||||
# | |||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
# SOFTWARE. | |||||
from fnmatch import fnmatch | |||||
import sqlite3 as sqlite | |||||
from threading import Lock | |||||
__all__ = ["PermissionsDB"] | |||||
class PermissionsDB(object): | |||||
""" | |||||
**EarwigBot: Permissions Database Manager** | |||||
Controls the :file:`permissions.db` file, which stores the bot's owners and | |||||
admins for the purposes of using certain dangerous IRC commands. | |||||
""" | |||||
ADMIN = 1 | |||||
OWNER = 2 | |||||
def __init__(self, dbfile): | |||||
self._dbfile = dbfile | |||||
self._db_access_lock = Lock() | |||||
self._data = {} | |||||
def __repr__(self): | |||||
"""Return the canonical string representation of the PermissionsDB.""" | |||||
res = "PermissionsDB(dbfile={0!r})" | |||||
return res.format(self._dbfile) | |||||
def __str__(self): | |||||
"""Return a nice string representation of the PermissionsDB.""" | |||||
return "<PermissionsDB at {0}>".format(self._dbfile) | |||||
def _create(self, conn): | |||||
"""Initialize the permissions database with its necessary tables.""" | |||||
query = """CREATE TABLE users (user_nick, user_ident, user_host, | |||||
user_rank)""" | |||||
conn.execute(query) | |||||
def _is_rank(self, user, rank): | |||||
"""Return True if the given user has the given rank, else False.""" | |||||
try: | |||||
for rule in self._data[rank]: | |||||
if user in rule: | |||||
return rule | |||||
except KeyError: | |||||
pass | |||||
return False | |||||
def _set_rank(self, user, rank): | |||||
"""Add a User to the database under a given rank.""" | |||||
try: | |||||
self._data[rank].append(user) | |||||
except KeyError: | |||||
self._data[rank] = [user] | |||||
query = "INSERT INTO users VALUES (?, ?, ?, ?)" | |||||
with sqlite.connect(self._dbfile) as conn, self._db_access_lock: | |||||
conn.execute(query, (user.nick, user.ident, user.host, rank)) | |||||
return user | |||||
def _del_rank(self, user, rank): | |||||
"""Remove a User from the database.""" | |||||
query = """DELETE FROM users WHERE user_nick = ? AND user_ident = ? AND | |||||
user_host = ? AND user_rank = ?""" | |||||
try: | |||||
for rule in self._data[rank]: | |||||
if user in rule: | |||||
with self._db_access_lock: | |||||
with sqlite.connect(self._dbfile) as conn: | |||||
args = (user.nick, user.ident, user.host, rank) | |||||
conn.execute(query, args) | |||||
return rule | |||||
except KeyError: | |||||
pass | |||||
return None | |||||
@property | |||||
def data(self): | |||||
"""A dict of all entries in the permissions database.""" | |||||
return self._data | |||||
def load(self): | |||||
"""Load permissions from an existing database, or create a new one.""" | |||||
query = "SELECT user_nick, user_ident, user_host, user_rank FROM users" | |||||
self._data = {} | |||||
with sqlite.connect(self._dbfile) as conn, self._db_access_lock: | |||||
try: | |||||
for nick, ident, host, rank in conn.execute(query): | |||||
try: | |||||
self._data[rank].append(_User(nick, ident, host)) | |||||
except KeyError: | |||||
self._data[rank] = [_User(nick, ident, host)] | |||||
except sqlite.OperationalError: | |||||
self._create(conn) | |||||
def has_exact(self, nick="*", ident="*", host="*", rule): | |||||
"""Return ``True`` if there is an exact match for this rule.""" | |||||
try: | |||||
for usr in self._data[rank]: | |||||
if nick != usr.nick or ident != usr.ident or host != usr.host: | |||||
continue | |||||
return usr | |||||
except KeyError: | |||||
pass | |||||
return False | |||||
def is_admin(self, data): | |||||
"""Return ``True`` if the given user is a bot admin, else ``False``.""" | |||||
user = _User(data.nick, data.ident, data.host) | |||||
return self._is_rank(user, rank=self.ADMIN) | |||||
def is_owner(self, data): | |||||
"""Return ``True`` if the given user is a bot owner, else ``False``.""" | |||||
user = _User(data.nick, data.ident, data.host) | |||||
return self._is_rank(user, rank=self.OWNER) | |||||
def add_admin(self, nick="*", ident="*", host="*"): | |||||
"""Add a nick/ident/host combo to the bot admins list.""" | |||||
return self._set_rank(_User(nick, ident, host), rank=self.ADMIN) | |||||
def add_owner(self, nick="*", ident="*", host="*"): | |||||
"""Add a nick/ident/host combo to the bot owners list.""" | |||||
return self._set_rank(_User(nick, ident, host), rank=self.OWNER) | |||||
def remove_admin(self, nick="*", ident="*", host="*"): | |||||
"""Remove a nick/ident/host combo to the bot admins list.""" | |||||
return self._del_rank(_User(nick, ident, host), rank=self.ADMIN) | |||||
def remove_owner(self, nick="*", ident="*", host="*"): | |||||
"""Remove a nick/ident/host combo to the bot owners list.""" | |||||
return self._del_rank(_User(nick, ident, host), rank=self.OWNER) | |||||
class _User(object): | |||||
"""A class that represents an IRC user for the purpose of testing rules.""" | |||||
def __init__(self, nick, ident, host): | |||||
self.nick = nick | |||||
self.ident = ident | |||||
self.host = host | |||||
def __repr__(self): | |||||
"""Return the canonical string representation of the User.""" | |||||
res = "_User(nick={0!r}, ident={1!r}, host={2!r})" | |||||
return res.format(self.nick, self.ident, self.host) | |||||
def __str__(self): | |||||
"""Return a nice string representation of the User.""" | |||||
return "{0}!{1}@{2}".format(self.nick, self.ident, self.host) | |||||
def __contains__(self, user): | |||||
if fnmatch(user.nick, self.nick): | |||||
if fnmatch(user.ident, self.ident): | |||||
if fnmatch(user.host, self.host): | |||||
return True | |||||
return False |
@@ -43,6 +43,7 @@ class IRCConnection(object): | |||||
self._send_lock = Lock() | self._send_lock = Lock() | ||||
self._last_recv = time() | self._last_recv = time() | ||||
self._last_send = 0 | |||||
self._last_ping = 0 | self._last_ping = 0 | ||||
def __repr__(self): | def __repr__(self): | ||||
@@ -87,6 +88,9 @@ class IRCConnection(object): | |||||
def _send(self, msg, hidelog=False): | def _send(self, msg, hidelog=False): | ||||
"""Send data to the server.""" | """Send data to the server.""" | ||||
with self._send_lock: | with self._send_lock: | ||||
time_since_last = time() - self._last_send | |||||
if time_since_last < 0.75: | |||||
time.sleep(0.75 - time_since_last) | |||||
try: | try: | ||||
self._sock.sendall(msg + "\r\n") | self._sock.sendall(msg + "\r\n") | ||||
except socket.error: | except socket.error: | ||||
@@ -94,6 +98,7 @@ class IRCConnection(object): | |||||
else: | else: | ||||
if not hidelog: | if not hidelog: | ||||
self.logger.debug(msg) | self.logger.debug(msg) | ||||
self._last_send = time() | |||||
def _split(self, msgs, maxlen, maxsplits=3): | def _split(self, msgs, maxlen, maxsplits=3): | ||||
"""Split a large message into multiple messages smaller than maxlen.""" | """Split a large message into multiple messages smaller than maxlen.""" | ||||
@@ -158,7 +163,7 @@ class IRCConnection(object): | |||||
def say(self, target, msg, hidelog=False): | def say(self, target, msg, hidelog=False): | ||||
"""Send a private message to a target on the server.""" | """Send a private message to a target on the server.""" | ||||
for msg in self._split(msg, 500 - len(target)): | |||||
for msg in self._split(msg, 400): | |||||
msg = "PRIVMSG {0} :{1}".format(target, msg) | msg = "PRIVMSG {0} :{1}".format(target, msg) | ||||
self._send(msg, hidelog) | self._send(msg, hidelog) | ||||
@@ -177,7 +182,7 @@ class IRCConnection(object): | |||||
def notice(self, target, msg, hidelog=False): | def notice(self, target, msg, hidelog=False): | ||||
"""Send a notice to a target on the server.""" | """Send a notice to a target on the server.""" | ||||
for msg in self._split(msg, 500 - len(target)): | |||||
for msg in self._split(msg, 400): | |||||
msg = "NOTICE {0} :{1}".format(target, msg) | msg = "NOTICE {0} :{1}".format(target, msg) | ||||
self._send(msg, hidelog) | self._send(msg, hidelog) | ||||
@@ -44,7 +44,7 @@ class ExclusionsDB(object): | |||||
""" | """ | ||||
**EarwigBot: Wiki Toolset: Exclusions Database Manager** | **EarwigBot: Wiki Toolset: Exclusions Database Manager** | ||||
Controls the :file:`.exclusions.db` file, which stores URLs excluded from | |||||
Controls the :file:`exclusions.db` file, which stores URLs excluded from | |||||
copyright violation checks on account of being known mirrors, for example. | copyright violation checks on account of being known mirrors, for example. | ||||
""" | """ | ||||