diff --git a/plugins/core/ignore.py b/plugins/core/ignore.py index 880c43189..55c190dde 100644 --- a/plugins/core/ignore.py +++ b/plugins/core/ignore.py @@ -1,8 +1,13 @@ +from collections import OrderedDict + from irclib.util.compare import match_mask -from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, String, Boolean +from sqlalchemy import ( + Boolean, Column, PrimaryKeyConstraint, String, Table, UniqueConstraint, and_, + select, +) from cloudbot import hook -from cloudbot.util import database +from cloudbot.util import database, web table = Table( "ignored", @@ -34,12 +39,15 @@ def load_cache(db): ignore_cache.extend(new_cache) +def ignore_in_cache(conn, chan, mask): + return (conn.casefold(), chan.casefold(), mask.casefold()) in ignore_cache + + def add_ignore(db, conn, chan, mask): - if (conn, chan) in ignore_cache: - pass - else: - db.execute(table.insert().values(connection=conn, channel=chan, mask=mask)) + if ignore_in_cache(conn, chan, mask): + return + db.execute(table.insert().values(connection=conn, channel=chan, mask=mask)) db.commit() load_cache(db) @@ -66,6 +74,8 @@ def is_ignored(conn, chan, mask): if match_mask(mask_cf, _mask_cf): return True + return False + # noinspection PyUnusedLocal @hook.sieve(priority=50) @@ -113,7 +123,7 @@ def ignore(text, db, chan, conn, notice, admin_log, nick): """ - ignores all input from in this channel.""" target = get_user(conn, text) - if is_ignored(conn.name, chan, target): + if ignore_in_cache(conn.name, chan, target): notice("{} is already ignored in {}.".format(target, chan)) else: admin_log("{} used IGNORE to make me ignore {} in {}".format(nick, target, chan)) @@ -126,7 +136,7 @@ def unignore(text, db, chan, conn, notice, nick, admin_log): """ - un-ignores all input from in this channel.""" target = get_user(conn, text) - if not is_ignored(conn.name, chan, target): + if not ignore_in_cache(conn.name, chan, target): notice("{} is not ignored in {}.".format(target, chan)) else: admin_log("{} used UNIGNORE to make me stop ignoring {} in {}".format(nick, target, chan)) @@ -134,12 +144,26 @@ def unignore(text, db, chan, conn, notice, nick, admin_log): remove_ignore(db, conn.name, chan, target) +@hook.command(permissions=["ignore", "chanop"], autohelp=False) +def listignores(db, conn, chan): + """- List all active ignores for the current channel""" + + rows = db.execute(select([table.c.mask], and_( + table.c.connection == conn.name.lower(), + table.c.channel == chan.lower(), + ))).fetchall() + + out = '\n'.join(row['mask'] for row in rows) + '\n' + + return web.paste(out) + + @hook.command(permissions=["botcontrol"]) def global_ignore(text, db, conn, notice, nick, admin_log): """ - ignores all input from in ALL channels.""" target = get_user(conn, text) - if is_ignored(conn.name, "*", target): + if ignore_in_cache(conn.name, "*", target): notice("{} is already globally ignored.".format(target)) else: notice("{} has been globally ignored.".format(target)) @@ -152,9 +176,41 @@ def global_unignore(text, db, conn, notice, nick, admin_log): """ - un-ignores all input from in ALL channels.""" target = get_user(conn, text) - if not is_ignored(conn.name, "*", target): + if not ignore_in_cache(conn.name, "*", target): notice("{} is not globally ignored.".format(target)) else: notice("{} has been globally un-ignored.".format(target)) admin_log("{} used GLOBAL_UNIGNORE to make me stop ignoring {} everywhere".format(nick, target)) remove_ignore(db, conn.name, "*", target) + + +@hook.command(permissions=["botcontrol", "snoonetstaff"], autohelp=False) +def list_global_ignores(db, conn): + """- List all global ignores for the current network""" + return listignores(db, conn, "*") + + +@hook.command(permissions=["botcontrol", "snoonetstaff"], autohelp=False) +def list_all_ignores(db, conn, text): + """ - List all ignores for , requires elevated permissions""" + whereclause = table.c.connection == conn.name.lower() + + if text: + whereclause = and_(whereclause, table.c.channel == text.lower()) + + rows = db.execute(select([table.c.channel, table.c.mask], whereclause)).fetchall() + + ignores = OrderedDict() + + for row in rows: + ignores.setdefault(row['channel'], []).append(row['mask']) + + out = "" + for chan, masks in ignores.items(): + out += "Ignores for {}:\n".format(chan) + for mask in masks: + out += '- {}\n'.format(mask) + + out += '\n' + + return web.paste(out) diff --git a/tests/plugin_tests/test_ignore.py b/tests/plugin_tests/test_ignore.py new file mode 100644 index 000000000..ad293a4b5 --- /dev/null +++ b/tests/plugin_tests/test_ignore.py @@ -0,0 +1,246 @@ +import pytest +from mock import MagicMock + + +class MockConn: + def __init__(self, name): + self.name = name + + +def setup_db(mock_db): + from plugins.core import ignore + + ignore.table.create(mock_db.engine, checkfirst=True) + + sess = mock_db.session() + + sess.execute(ignore.table.delete()) + + ignore.load_cache(sess) + + +def test_ignore(mock_db, patch_paste): + from plugins.core import ignore + + setup_db(mock_db) + + sess = mock_db.session() + + conn = MockConn('testconn') + + ignore.add_ignore(sess, conn.name, '#chan', '*!*@host') + ignore.add_ignore(sess, conn.name, '#chan', '*!*@host') + + ignore.add_ignore(sess, conn.name, '*', '*!*@evil.host') + + assert ignore.is_ignored(conn.name, '#chan', 'nick!user@host') + assert ignore.is_ignored(conn.name, '#a_chan', 'evil!user@evil.host') + + assert not ignore.is_ignored(conn.name, '#chan', 'nick!user@otherhost') + + assert not ignore.is_ignored(conn.name, '#otherchan', 'nick!user@host') + + assert not ignore.is_ignored('otherconn', '#chan', 'nick!user@host') + + ignore.listignores(sess, conn, '#chan') + + patch_paste.assert_called_with('*!*@host\n') + + ignore.list_all_ignores(sess, conn, '') + + patch_paste.assert_called_with( + 'Ignores for #chan:\n- *!*@host\n\nIgnores for *:\n- *!*@evil.host\n\n' + ) + + ignore.list_all_ignores(sess, conn, '#chan') + + patch_paste.assert_called_with('Ignores for #chan:\n- *!*@host\n\n') + + ignore.list_global_ignores(sess, conn) + + patch_paste.assert_called_with('*!*@evil.host\n') + + +def test_remove_ignore(mock_db): + from plugins.core import ignore + + setup_db(mock_db) + + sess = mock_db.session() + + ignore.add_ignore(sess, 'testconn', '#chan', '*!*@host') + + assert ignore.is_ignored('testconn', '#chan', 'nick!user@host') + + ignore.remove_ignore(sess, 'testconn', '#chan', '*!*@host') + + assert not ignore.is_ignored('testconn', '#chan', 'nick!user@host') + + +@pytest.mark.asyncio +async def test_ignore_sieve(mock_db): + from plugins.core import ignore + + setup_db(mock_db) + + sess = mock_db.session() + + ignore.add_ignore(sess, 'testconn', '#chan', '*!*@host') + + _hook = MagicMock() + bot = MagicMock() + event = MagicMock() + + _hook.type = "irc_raw" + + assert (await ignore.ignore_sieve(bot, event, _hook)) is event + + _hook.type = "command" + event.triggered_command = "unignore" + + assert (await ignore.ignore_sieve(bot, event, _hook)) is event + + event.triggered_command = "somecommand" + + event.mask = None + + assert (await ignore.ignore_sieve(bot, event, _hook)) is event + + event.conn.name = "testconn" + event.chan = "#chan" + event.mask = "nick!user@host" + + assert (await ignore.ignore_sieve(bot, event, _hook)) is None + + event.conn.name = "testconn1" + + assert (await ignore.ignore_sieve(bot, event, _hook)) is event + + +def test_get_user(): + from plugins.core import ignore + + conn = MagicMock() + + conn.memory = {} + + assert ignore.get_user(conn, "nick") == "nick!*@*" + assert ignore.get_user(conn, "nick!user@host") == "nick!user@host" + + conn.memory["users"] = {"nick": {"nick": "nick", "user": "user", "host": "host"}} + + assert ignore.get_user(conn, "nick") == "*!*@host" + + +def test_ignore_command(mock_db): + from plugins.core import ignore + + setup_db(mock_db) + + sess = mock_db.session() + + conn = MagicMock() + + conn.name = "testconn" + conn.memory = {} + + event = MagicMock() + + ignore.ignore( + "*!*@host", sess, "#chan", conn, event.notice, event.admin_log, "opnick" + ) + + event.admin_log.assert_called_with( + 'opnick used IGNORE to make me ignore *!*@host in #chan' + ) + event.notice.assert_called_with('*!*@host has been ignored in #chan.') + assert ignore.is_ignored('testconn', '#chan', 'nick!user@host') + + event.reset_mock() + + ignore.global_ignore( + "*!*@host", sess, conn, event.notice, "opnick", event.admin_log + ) + + event.admin_log.assert_called_with( + 'opnick used GLOBAL_IGNORE to make me ignore *!*@host everywhere' + ) + event.notice.assert_called_with('*!*@host has been globally ignored.') + assert ignore.is_ignored('testconn', '#otherchan', 'nick!user@host') + + event.reset_mock() + + ignore.ignore( + "*!*@host", sess, "#chan", conn, event.notice, event.admin_log, "opnick" + ) + + event.notice.assert_called_with('*!*@host is already ignored in #chan.') + + event.reset_mock() + + ignore.global_ignore( + "*!*@host", sess, conn, event.notice, "opnick", event.admin_log + ) + + event.notice.assert_called_with('*!*@host is already globally ignored.') + + +def test_unignore_command(mock_db): + from plugins.core import ignore + + setup_db(mock_db) + + sess = mock_db.session() + + conn = MagicMock() + + conn.name = "testconn" + conn.memory = {} + + event = MagicMock() + + ignore.unignore( + "*!*@host", sess, "#chan", conn, event.notice, "opnick", event.admin_log + ) + + event.notice.assert_called_with("*!*@host is not ignored in #chan.") + + ignore.add_ignore(sess, 'testconn', '#chan', '*!*@host') + + event.reset_mock() + + ignore.unignore( + "*!*@host", sess, "#chan", conn, event.notice, "opnick", event.admin_log + ) + + event.notice.assert_called_with('*!*@host has been un-ignored in #chan.') + event.admin_log.assert_called_with( + 'opnick used UNIGNORE to make me stop ignoring *!*@host in #chan' + ) + + assert not ignore.is_ignored("testconn", "#chan", "nick!user@host") + + event.reset_mock() + + ignore.global_unignore( + "*!*@host", sess, conn, event.notice, "opnick", event.admin_log + ) + + event.notice.assert_called_with('*!*@host is not globally ignored.') + + ignore.add_ignore(sess, 'testconn', '*', '*!*@host') + + event.reset_mock() + + ignore.global_unignore( + "*!*@host", sess, conn, event.notice, "opnick", event.admin_log + ) + + event.notice.assert_called_with('*!*@host has been globally un-ignored.') + event.admin_log.assert_called_with( + 'opnick used GLOBAL_UNIGNORE to make me stop ignoring *!*@host everywhere' + ) + + assert not ignore.is_ignored("testconn", "#chan", "nick!user@host") + + event.reset_mock()