Browse Source

Merge branch 'feature/permissions' into develop (#2)

tags/v0.1^2
Ben Kurtovic 12 years ago
parent
commit
39740a65cd
11 changed files with 496 additions and 97 deletions
  1. +141
    -0
      earwigbot/commands/access.py
  2. +1
    -1
      earwigbot/commands/chanops.py
  3. +2
    -2
      earwigbot/commands/git_command.py
  4. +1
    -1
      earwigbot/commands/quit.py
  5. +1
    -1
      earwigbot/commands/threads.py
  6. +20
    -89
      earwigbot/config/__init__.py
  7. +51
    -0
      earwigbot/config/formatter.py
  8. +97
    -0
      earwigbot/config/node.py
  9. +174
    -0
      earwigbot/config/permissions.py
  10. +7
    -2
      earwigbot/irc/connection.py
  11. +1
    -1
      earwigbot/wiki/copyvios/exclusions.py

+ 141
- 0
earwigbot/commands/access.py View File

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

+ 1
- 1
earwigbot/commands/chanops.py View File

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




+ 2
- 2
earwigbot/commands/git_command.py View File

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


+ 1
- 1
earwigbot/commands/quit.py View File

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


+ 1
- 1
earwigbot/commands/threads.py View File

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


earwigbot/config.py → earwigbot/config/__init__.py View File

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

+ 51
- 0
earwigbot/config/formatter.py View File

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

+ 97
- 0
earwigbot/config/node.py View File

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

+ 174
- 0
earwigbot/config/permissions.py View File

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

+ 7
- 2
earwigbot/irc/connection.py View File

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




+ 1
- 1
earwigbot/wiki/copyvios/exclusions.py View File

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




Loading…
Cancel
Save