diff --git a/csbot.deploy.cfg b/csbot.deploy.cfg index aa3dd2ae..6cc5fe2f 100644 --- a/csbot.deploy.cfg +++ b/csbot.deploy.cfg @@ -1,7 +1,12 @@ [@bot] nickname = Mathison channels = #cs-york #cs-york-dev -plugins = logger linkinfo hoogle imgur csyork +plugins = logger linkinfo hoogle imgur csyork auth [linkinfo] scan_limit = 2 + +[auth] +@everything = * *:* +Alan = @everything +Haegin = @everything \ No newline at end of file diff --git a/csbot/core.py b/csbot/core.py index 9214b8e5..47de45ee 100644 --- a/csbot/core.py +++ b/csbot/core.py @@ -59,6 +59,7 @@ def __init__(self, config=None): # Load configuration self.config_root = configparser.ConfigParser(interpolation=None, allow_no_value=True) + self.config_root.optionxform = str # No lowercase option names if config is not None: self.config_root.read_file(config) @@ -164,6 +165,8 @@ class PluginError(Exception): class BotProtocol(irc.IRCClient): log = logging.getLogger('csbot.protocol') + _WHO_IDENTIFY = ('1', '%na') + def __init__(self, bot): self.bot = bot # Get IRCClient configuration from the Bot @@ -192,6 +195,7 @@ def emit(self, event): def connectionMade(self): irc.IRCClient.connectionMade(self) + self.sendLine('CAP REQ :account-notify extended-join') self.emit_new('core.raw.connected') def connectionLost(self, reason): @@ -219,6 +223,7 @@ def signedOn(self): self.emit_new('core.self.connected') def joined(self, channel): + self.identify(channel) self.emit_new('core.self.joined', {'channel': channel}) def left(self, channel): @@ -251,11 +256,24 @@ def action(self, user, channel, message): 'reply_to': nick(user) if channel == self.nickname else channel, }) - def userJoined(self, user, channel): - self.emit_new('core.channel.joined', { - 'channel': channel, - 'user': user, - }) + def irc_JOIN(self, prefix, params): + """Re-implement ``JOIN`` handler to account for ``extended-join`` info. + """ + user = prefix + nick_ = nick(user) + channel, account, _ = params + + if nick_ == self.nickname: + self.joined(channel) + else: + self.emit_new('core.user.identified', { + 'user': user, + 'account': None if account == '*' else account, + }) + self.emit_new('core.channel.joined', { + 'channel': channel, + 'user': user, + }) def userLeft(self, user, channel): self.emit_new('core.channel.left', { @@ -315,6 +333,28 @@ def topicUpdated(self, user, channel, newtopic): 'topic': newtopic, }) + def identify(self, target): + """Find the account for a user or all users in a channel.""" + tag, query = self._WHO_IDENTIFY + self.sendLine('WHO {} {}t,{}'.format(target, query, tag)) + pass + + def irc_354(self, prefix, params): + """Handle "formatted WHO" responses.""" + tag = params[1] + if tag == self._WHO_IDENTIFY[0]: + self.emit_new('core.user.identified', { + 'user': params[2], + 'account': None if params[3] == '0' else params[3], + }) + + def irc_ACCOUNT(self, prefix, params): + """Account change notification from ``account-notify`` capability.""" + self.emit_new('core.user.identified', { + 'user': prefix, + 'account': None if params[0] == '*' else params[0], + }) + class BotFactory(protocol.ClientFactory): def __init__(self, bot): @@ -368,7 +408,7 @@ def main(argv): handler.setLevel(args.loglevel) handler.addFilter(ColorLogFilter()) handler.setFormatter(logging.Formatter( - ('\x1b[%(color)sm[%(asctime)s] (%(levelname).1s:%(name)s)' + ('\x1b[%(color)sm[%(asctime)s] (%(levelname).1s:%(name)s) ' '%(message)s\x1b[0m'), '%Y/%m/%d %H:%M:%S')) rootlogger = logging.getLogger('') diff --git a/csbot/plugins/auth.py b/csbot/plugins/auth.py new file mode 100644 index 00000000..992d2f17 --- /dev/null +++ b/csbot/plugins/auth.py @@ -0,0 +1,106 @@ +from collections import defaultdict + +from csbot.plugin import Plugin + + +class PermissionDB(defaultdict): + """A helper class for assembling the permissions database.""" + def __init__(self): + super(PermissionDB, self).__init__(set) + self._groups = defaultdict(set) + self._current = None + + def process(self, entity, permissions): + """Process a configuration entry, where *entity* is an account name, + ``@group`` name or ``*`` and *permissions* is a space-separated list of + permissions to grant. + """ + self._current = (entity, permissions) + + if entity.startswith('@'): + if entity in self._groups: + self._error('Group "{}" already defined'.format(entity)) + entity_perms = self._groups[entity] + else: + entity_perms = self[entity] + + for permission in permissions.split(): + if ':' in permission: + self._add_channel_permissions(entity_perms, permission) + elif permission.startswith('@'): + self._copy_group_permissions(entity_perms, permission) + else: + self._add_bot_permission(entity_perms, permission) + + self._current = None + + def get_permissions(self, entity): + """Get the set of permissions for *entity*. + + The union of the permissions for *entity* and the universal (``*``) + permissions is returned. If *entity* is ``None``, only the universal + permissions are returned. + """ + if entity is None: + return set(self.get('*', set())) + else: + return self.get(entity, set()) | self.get('*', set()) + + def check(self, entity, permission, channel=None): + """Check if *entity* has *permission*. + + If *channel* is present, check for a channel permission, otherwise check + for a bot permission. Compatible wildcard permissions are also checked. + """ + if channel is None: + checks = {permission, '*'} + else: + checks = {(channel, permission), (channel, '*'), + ('*', permission), ('*', '*')} + + return len(checks & self.get_permissions(entity)) > 0 + + def _add_channel_permissions(self, entity_perms, permission): + channel, _, permissions = permission.partition(':') + + if not (channel == '*' or channel.startswith('#')): + self._error('Invalid channel name "{}", must be * or #channel' + .format(channel)) + if permissions == '': + self._error('No permissions specified for channel "{}"' + .format(channel)) + + for p in permissions.split(','): + entity_perms.add((channel, p)) + + def _copy_group_permissions(self, entity_perms, group): + if group not in self._groups: + self._error('Permission group "{}" undefined'.format(group)) + if group == self._current[0]: + self._error('Recursive group definition') + entity_perms.update(self._groups[group]) + + def _add_bot_permission(self, entity_perms, permission): + entity_perms.add(permission) + + def _error(self, s, *args): + entity, perms = self._current + raise ValueError('{} (in: {} = {})'.format(s, entity, perms), *args) + + +class Auth(Plugin): + PLUGIN_DEPENDS = ['usertrack'] + + def setup(self): + super(Auth, self).setup() + + self._permissions = PermissionDB() + for entity, permissions in self.config.items(): + self._permissions.process(entity, permissions) + + for e, p in self._permissions.iteritems(): + self.log.debug((e, p)) + + def check(self, nick, perm, channel=None): + account = self.bot.plugins['usertrack'].get_user(nick)['account'] + return self._permissions.check(account, perm, channel) \ No newline at end of file diff --git a/csbot/plugins/usertrack.py b/csbot/plugins/usertrack.py new file mode 100644 index 00000000..be55e4f3 --- /dev/null +++ b/csbot/plugins/usertrack.py @@ -0,0 +1,89 @@ +from collections import defaultdict +from copy import deepcopy + +from csbot.plugin import Plugin +from csbot.util import nick + + +class UserDict(defaultdict): + def __missing__(self, key): + user = self.create_user(key) + self[key] = user + return user + + @staticmethod + def create_user(nick): + return { + 'nick': nick, + 'account': None, + 'channels': set(), + } + + def copy_or_create(self, nick): + if nick in self: + return deepcopy(self[nick]) + else: + return self.create_user(nick) + + +class UserTrack(Plugin): + def setup(self): + super(UserTrack, self).setup() + self._users = UserDict() + + @Plugin.hook('core.channel.joined') + def _channel_joined(self, e): + user = self._users[nick(e['user'])] + user['channels'].add(e['channel']) + + @Plugin.hook('core.channel.left') + def _channel_left(self, e): + user = self._users[nick(e['user'])] + user['channels'].discard(e['channel']) + # Lost sight of the user, can't reliably track them any more + if len(user['channels']) == 0: + del self._users[nick(e['user'])] + + @Plugin.hook('core.channel.names') + def _channel_names(self, e): + for name, prefixes in e['names']: + user = self._users[name] + user['channels'].add(e['channel']) + + @Plugin.hook('core.user.identified') + def _user_identified(self, e): + user = self._users[nick(e['user'])] + user['account'] = e['account'] + + @Plugin.hook('core.user.renamed') + def _user_renamed(self, e): + # Retrieve user record + user = self._users[e['oldnick']] + # Remove old nick entry + del self._users[user['nick']] + # Rename user + user['nick'] = e['newnick'] + # Add under new nick + self._users[user['nick']] = user + + @Plugin.hook('core.user.quit') + def _user_quit(self, e): + # User is gone, remove record + del self._users[nick(e['user'])] + + def get_user(self, nick): + """Get a copy of the user record for *nick*. + """ + return self._users.copy_or_create(nick) + + @Plugin.command('account', help=('account [nick]: show Freenode account for' + ' a nick, or for yourself if omitted')) + def account_command(self, e): + nick_ = e['data'] or nick(e['user']) + account = self.get_user(nick_)['account'] + if account is None: + e.protocol.msg(e['reply_to'], + u'{} is not authenticated'.format(nick_)) + else: + e.protocol.msg(e['reply_to'], + u'{} is authenticated as {}'.format(nick_, account)) \ No newline at end of file diff --git a/csbot/test/helpers.py b/csbot/test/helpers.py index a07eac90..6621e7f6 100644 --- a/csbot/test/helpers.py +++ b/csbot/test/helpers.py @@ -1,4 +1,11 @@ import os +from StringIO import StringIO +from textwrap import dedent + +from twisted.trial import unittest +from twisted.test import proto_helpers + +from csbot.core import Bot, BotProtocol class TempEnvVars(object): @@ -21,3 +28,35 @@ def __exit__(self, exc_type, exc_value, traceback): os.environ[k] = self.restore[k] else: del os.environ[k] + + +class BotTestCase(unittest.TestCase): + """Common functionality for bot test case. + + A :class:`unittest.TestCase` with bot and plugin test fixtures. Creates a + bot from :attr:`CONFIG`, binding it to ``self.bot``, and also binding every + plugin in :attr:`PLUGINS` to ``self.plugin``. + """ + CONFIG = "" + PLUGINS = [] + + def setUp(self): + """Create bot and plugin bindings.""" + # Bot and protocol stuff, suffixed with _ so they can't clash with + # possible/likely plugin names + self.bot_ = Bot(StringIO(dedent(self.CONFIG))) + self.bot_.bot_setup() + self.transport_ = proto_helpers.StringTransport() + self.protocol_ = BotProtocol(self.bot_) + self.protocol_.transport = self.transport_ + + for p in self.PLUGINS: + setattr(self, p, self.bot_.plugins[p]) + + def tearDown(self): + """Lose references to bot and plugins.""" + self.bot_ = None + self.transport_ = None + self.protocol_ = None + for p in self.PLUGINS: + setattr(self, p, None) diff --git a/csbot/test/plugins/test_auth.py b/csbot/test/plugins/test_auth.py new file mode 100644 index 00000000..82ba0cb4 --- /dev/null +++ b/csbot/test/plugins/test_auth.py @@ -0,0 +1,129 @@ +from StringIO import StringIO + +from twisted.trial import unittest + + +from csbot.core import Bot +from csbot.plugins.auth import Auth, PermissionDB + + +class TestPermissionDB(unittest.TestCase): + def setUp(self): + self.permissions = PermissionDB() + + def tearDown(self): + self.permissions = None + + def test_channel_permissions(self): + """Check that channel permissions are interpreted correctly.""" + self.permissions.process('User1', '#hello:world #foo:bar,baz *:topic #channel:*') + self.assertEqual(self.permissions.get_permissions('User1'), { + ('#hello', 'world'), + ('#foo', 'bar'), ('#foo', 'baz'), + ('*', 'topic'), + ('#channel', '*'), + }) + + def test_invalid_channel_permissions(self): + """Check that invalid channel permissions aren't accepted.""" + self.assertRaises(ValueError, self.permissions.process, + 'User1', 'foo:bar') + self.assertRaises(ValueError, self.permissions.process, + 'User1', 'foo:') + self.assertRaises(ValueError, self.permissions.process, + 'User1', ':bar') + self.assertRaises(ValueError, self.permissions.process, + 'User1', ':') + + def test_bot_permissions(self): + """Check that bot (non-channel) permissions are interpreted correctly.""" + self.permissions.process('User1', 'hello world *') + self.assertEqual(self.permissions.get_permissions('User1'), { + 'hello', 'world', '*', + }) + + def test_group_permissions(self): + """Check that permission groups result in users getting the correct permissions.""" + self.permissions.process('@group', '#foo:a,b #bar:* baz') + self.permissions.process('User1', '@group') + self.assertEqual(self.permissions.get_permissions('User1'), { + ('#foo', 'a'), ('#foo', 'b'), + ('#bar', '*'), + 'baz', + }) + + def test_undefined_group(self): + """Check that undefined groups raise errors.""" + self.assertRaises(ValueError, self.permissions.process, + 'User1', '@group') + + def test_recursive_group(self): + """Check that a recursive group raises errors.""" + self.assertRaises(ValueError, self.permissions.process, + '@group', '@group') + + def test_redefined_group(self): + """Check that redefined groups raise errors.""" + self.permissions.process('@group', 'foo') + self.assertRaises(ValueError, self.permissions.process, + '@group', 'bar') + + def test_universal_permissions(self): + """Check that users get permissions granted to everybody.""" + self.permissions.process('*', '#boring-channel:*') + self.permissions.process('User1', '#other-channel:topic') + self.assertEqual(self.permissions.get_permissions('User1'), { + ('#boring-channel', '*'), ('#other-channel', 'topic'), + }) + self.assertEqual(self.permissions.get_permissions(None), { + ('#boring-channel', '*'), + }) + + def test_check_exact_channel_permission(self): + """Check that exact channel permission checks work.""" + self.permissions.process('User1', '#channel:foo') + self.assertTrue(self.permissions.check('User1', 'foo', '#channel')) + self.assertFalse(self.permissions.check('User1', 'foo', '#other-channel')) + self.assertFalse(self.permissions.check('User1', 'bar', '#channel')) + self.assertFalse(self.permissions.check('User2', 'foo', '#channel')) + # Ensure channel and bot permissions are not confused + self.assertFalse(self.permissions.check('User1', 'foo')) + + def test_check_exact_bot_permission(self): + """Check that exact bot permission checks work.""" + self.permissions.process('User1', 'foo') + self.assertTrue(self.permissions.check('User1', 'foo')) + self.assertFalse(self.permissions.check('User1', 'bar')) + self.assertFalse(self.permissions.check('User2', 'foo')) + # Ensure channel and bot permissions are not confused + self.assertFalse(self.permissions.check('User1', 'foo', '#channel')) + + def test_check_wildcard_channel_permission(self): + """Check that wildcard channel permissions work.""" + self.permissions.process('User1', '#channel:*') + self.permissions.process('User2', '*:foo') + self.permissions.process('User3', '*:*') + # Test permission wildcard with fixed channel + self.assertTrue(self.permissions.check('User1', 'foo', '#channel')) + self.assertTrue(self.permissions.check('User1', 'bar', '#channel')) + self.assertFalse(self.permissions.check('User1', 'foo', '#other')) + # Test channel wildcard with fixed permission + self.assertTrue(self.permissions.check('User2', 'foo', '#channel')) + self.assertTrue(self.permissions.check('User2', 'foo', '#other')) + self.assertFalse(self.permissions.check('User2', 'bar', '#channel')) + # Test channel and permission wildcard + self.assertTrue(self.permissions.check('User3', 'foo', '#other')) + self.assertTrue(self.permissions.check('User3', 'bar', '#channel')) + # Ensure channel and bot permissions are not confused + self.assertFalse(self.permissions.check('User1', 'foo')) + self.assertFalse(self.permissions.check('User2', 'foo')) + self.assertFalse(self.permissions.check('User3', 'foo')) + + def test_check_wildcard_bot_permission(self): + """Check that wildcard bot permissions work.""" + self.permissions.process('User1', '*') + self.assertTrue(self.permissions.check('User1', 'foo')) + self.assertTrue(self.permissions.check('User1', 'bar')) + self.assertFalse(self.permissions.check('User2', 'foo')) + # Ensure channel and bot permissions are not confused + self.assertFalse(self.permissions.check('User1', 'foo', '#channel')) diff --git a/csbot/test/plugins/test_linkinfo.py b/csbot/test/plugins/test_linkinfo.py index 38959db8..88a244a5 100644 --- a/csbot/test/plugins/test_linkinfo.py +++ b/csbot/test/plugins/test_linkinfo.py @@ -1,18 +1,9 @@ # coding=utf-8 -from StringIO import StringIO - -from twisted.trial import unittest from httpretty import httprettified, HTTPretty from lxml.etree import LIBXML_VERSION -from csbot.core import Bot -from csbot.plugins.linkinfo import LinkInfo - +from ..helpers import BotTestCase -bot_config = """ -[@bot] -plugins = linkinfo -""" #: Test encoding handling; tests are (url, content-type, body, expected_title) encoding_test_cases = [ @@ -107,10 +98,13 @@ ] -class TestLinkInfoPlugin(unittest.TestCase): - def setUp(self): - self.bot = Bot(StringIO(bot_config)) - self.linkinfo = self.bot.plugins['linkinfo'] +class TestLinkInfoPlugin(BotTestCase): + CONFIG = """\ + [@bot] + plugins = linkinfo + """ + + PLUGINS = ['linkinfo'] @httprettified def test_encoding_handling(self): diff --git a/csbot/test/plugins/test_usertrack.py b/csbot/test/plugins/test_usertrack.py new file mode 100644 index 00000000..636e83c5 --- /dev/null +++ b/csbot/test/plugins/test_usertrack.py @@ -0,0 +1,116 @@ +from twisted.trial import unittest + +from ..helpers import BotTestCase + + +class TestUserTrackPlugin(BotTestCase): + CONFIG = """\ + [@bot] + plugins = usertrack + """ + + PLUGINS = ['usertrack'] + + def _assert_channels(self, nick, channels): + self.assertEqual(self.usertrack.get_user(nick)['channels'], channels) + + def _assert_account(self, nick, account): + self.assertEqual(self.usertrack.get_user(nick)['account'], account) + + def test_join_part(self): + self._assert_channels('Nick', set()) + self.protocol_.lineReceived(":Nick!~user@hostname JOIN #channel accountname :Other Info") + self._assert_channels('Nick', {'#channel'}) + + self._assert_channels('Other', set()) + self.protocol_.lineReceived(":Other!~user@hostname JOIN #channel accountname :Other Info") + self._assert_channels('Other', {'#channel'}) + + self.protocol_.lineReceived(":Other!~user@hostname JOIN #other accountname :Other Info") + self._assert_channels('Other', {'#channel', '#other'}) + self._assert_channels('Nick', {'#channel'}) + + self.protocol_.lineReceived(":Other!~user@hostname PART #channel") + self._assert_channels('Other', {'#other'}) + + def test_join_names(self): + raise unittest.SkipTest('NAMES messages too dependent on stateful IRCClient operation, ' + 'too difficult to test') + self._assert_channels('Nick', set()) + self._assert_channels('Other', set()) + # Initialise "PREFIX" feature so "NAMES" support works correctly + self.protocol_.lineReceived(":server 005 self PREFIX=(ov)@+ :are supported by this server") + self.protocol_.lineReceived(":server 353 self @ #channel :Nick Other") + self.protocol_.lineReceived(":server 366 self #channel :End of /NAMES list.") + self._assert_channels('Nick', {'#channel'}) + self._assert_channels('Other', {'#channel'}) + + def test_quit_channels(self): + self.protocol_.lineReceived(":Nick!~user@hostname JOIN #channel * :Other Info") + self._assert_channels('Nick', {'#channel'}) + self.protocol_.lineReceived(":Nick!~user@hostname QUIT :Quit message") + self._assert_channels('Nick', set()) + + def test_nick_changed(self): + self._assert_channels('Nick', set()) + self.protocol_.lineReceived(":Nick!~user@hostname JOIN #channel * :Other Info") + self._assert_channels('Nick', {'#channel'}) + self._assert_channels('Other', set()) + self.assertEqual(self.usertrack.get_user('Nick')['nick'], 'Nick') + self.protocol_.lineReceived(':Nick!~user@hostname NICK :Other') + self._assert_channels('Nick', set()) + self._assert_channels('Other', {'#channel'}) + self.assertEqual(self.usertrack.get_user('Other')['nick'], 'Other') + + def test_account_discovery_on_join(self): + self._assert_account('Nick', None) + # Check that account name is discovered from "extended join" information + self.protocol_.lineReceived(":Nick!~user@hostname JOIN #channel accountname :Other Info") + self._assert_account('Nick', 'accountname') + # Check that * is interpreted as "not authenticated" + self.protocol_.lineReceived(":Nick!~user@hostname JOIN #channel * :Other Info") + self._assert_account('Nick', None) + + def test_account_forgotten_on_lost_visibility(self): + # User joins channels, account discovered by extended-join + self._assert_account('Nick', None) + self.protocol_.lineReceived(":Nick!~user@hostname JOIN #channel accountname :Other Info") + self._assert_account('Nick', 'accountname') + self.protocol_.lineReceived(":Nick!~user@hostname JOIN #other accountname :Other Info") + self._assert_channels('Nick', {'#channel', '#other'}) + # User leaves one channel, account should still be known because user is still visible + self.protocol_.lineReceived(":Nick!~user@hostname PART #channel") + self._assert_account('Nick', 'accountname') + self._assert_channels('Nick', {'#other'}) + # User leaves last remaining channel, account should be forgotten + self.protocol_.lineReceived(":Nick!~user@hostname PART #other") + self._assert_account('Nick', None) + + def test_account_forgotten_on_quit(self): + self.protocol_.lineReceived(":Nick!~user@hostname JOIN #channel accountname :Other Info") + self._assert_account('Nick', 'accountname') + self.protocol_.lineReceived(":Nick!~user@hostname QUIT :Quit message") + self._assert_account('Nick', None) + + def test_account_notify(self): + self._assert_account('Nick', None) + self._assert_channels('Nick', set()) + self.protocol_.lineReceived(":Nick!~user@hostname JOIN #channel * :Other Info") + self._assert_account('Nick', None) + self._assert_channels('Nick', {'#channel'}) + self.protocol_.lineReceived(":Nick!~user@hostname ACCOUNT accountname") + self._assert_account('Nick', 'accountname') + self._assert_channels('Nick', {'#channel'}) + self.protocol_.lineReceived(":Nick!~user@hostname ACCOUNT *") + self._assert_account('Nick', None) + self._assert_channels('Nick', {'#channel'}) + + def test_account_kept_on_nick_changed(self): + self._assert_account('Nick', None) + self._assert_account('Other', None) + self.protocol_.lineReceived(":Nick!~user@hostname JOIN #channel accountname :Other Info") + self._assert_account('Nick', 'accountname') + self._assert_account('Other', None) + self.protocol_.lineReceived(':Nick!~user@hostname NICK :Other') + self._assert_account('Nick', None) + self._assert_account('Other', 'accountname') diff --git a/doc/api/csbot.plugins.rst b/doc/api/csbot.plugins.rst index ec158379..47c9a46f 100644 --- a/doc/api/csbot.plugins.rst +++ b/doc/api/csbot.plugins.rst @@ -9,6 +9,22 @@ plugins Package :undoc-members: :show-inheritance: +:mod:`auth` Module +------------------ + +.. automodule:: csbot.plugins.auth + :members: + :undoc-members: + :show-inheritance: + +:mod:`csyork` Module +-------------------- + +.. automodule:: csbot.plugins.csyork + :members: + :undoc-members: + :show-inheritance: + :mod:`example` Module --------------------- @@ -17,10 +33,26 @@ plugins Package :undoc-members: :show-inheritance: -:mod:`manager` Module ---------------------- +:mod:`hoogle` Module +-------------------- -.. automodule:: csbot.plugins.manager +.. automodule:: csbot.plugins.hoogle + :members: + :undoc-members: + :show-inheritance: + +:mod:`imgur` Module +------------------- + +.. automodule:: csbot.plugins.imgur + :members: + :undoc-members: + :show-inheritance: + +:mod:`linkinfo` Module +---------------------- + +.. automodule:: csbot.plugins.linkinfo :members: :undoc-members: :show-inheritance: @@ -33,18 +65,18 @@ plugins Package :undoc-members: :show-inheritance: -:mod:`manager` Module +:mod:`mongodb` Module --------------------- -.. automodule:: csbot.plugins.manager +.. automodule:: csbot.plugins.mongodb :members: :undoc-members: :show-inheritance: -:mod:`users` Module -------------------- +:mod:`usertrack` Module +----------------------- -.. automodule:: csbot.plugins.users +.. automodule:: csbot.plugins.usertrack :members: :undoc-members: :show-inheritance: diff --git a/doc/api/csbot.rst b/doc/api/csbot.rst index e1e73b6e..9de9b356 100644 --- a/doc/api/csbot.rst +++ b/doc/api/csbot.rst @@ -4,6 +4,10 @@ csbot Package :mod:`csbot` Package -------------------- +.. automodule:: csbot.__init__ + :members: + :undoc-members: + :show-inheritance: :mod:`core` Module ------------------ @@ -43,4 +47,5 @@ Subpackages .. toctree:: csbot.plugins + csbot.test diff --git a/doc/api/csbot.test.plugins.rst b/doc/api/csbot.test.plugins.rst new file mode 100644 index 00000000..552837ee --- /dev/null +++ b/doc/api/csbot.test.plugins.rst @@ -0,0 +1,19 @@ +plugins Package +=============== + +:mod:`test_auth` Module +----------------------- + +.. automodule:: csbot.test.plugins.test_auth + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_linkinfo` Module +--------------------------- + +.. automodule:: csbot.test.plugins.test_linkinfo + :members: + :undoc-members: + :show-inheritance: + diff --git a/doc/api/csbot.test.rst b/doc/api/csbot.test.rst new file mode 100644 index 00000000..661e44d1 --- /dev/null +++ b/doc/api/csbot.test.rst @@ -0,0 +1,34 @@ +test Package +============ + +:mod:`helpers` Module +--------------------- + +.. automodule:: csbot.test.helpers + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_config` Module +------------------------- + +.. automodule:: csbot.test.test_config + :members: + :undoc-members: + :show-inheritance: + +:mod:`test_events` Module +------------------------- + +.. automodule:: csbot.test.test_events + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + + csbot.test.plugins + diff --git a/requirements.txt b/requirements.txt index 7ce133e7..86bf3d5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Requirements for deployment Twisted>=12.0.0 -straight.plugin>=1.3 +straight.plugin==1.4.0-post-1 pymongo>=2.4.0 configparser>=3.2 requests>=0.14.0,<1.0.0