From 10152d79ee5b3d0d1ffbc5766823ce76769f8921 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 26 May 2023 21:14:28 +0200 Subject: [PATCH 1/4] Request the draft/message-redaction capability --- src/irclib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/irclib.py b/src/irclib.py index 6929ebb70..82d1089c2 100644 --- a/src/irclib.py +++ b/src/irclib.py @@ -1786,7 +1786,7 @@ def resetSasl(self): """ REQUEST_EXPERIMENTAL_CAPABILITIES = set(['draft/account-registration', - 'draft/multiline']) + 'draft/message-redaction', 'draft/multiline']) """Like REQUEST_CAPABILITIES, but these capabilities are only requested if supybot.protocols.irc.experimentalExtensions is enabled.""" From f00ecdeaefbaf5b6cca2c29ed98d72319090418f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 26 May 2023 21:15:12 +0200 Subject: [PATCH 2/4] BadWords: Add the option to redact messages containing bad words instead of or in addition to kicking --- plugins/BadWords/config.py | 5 +++ plugins/BadWords/plugin.py | 63 ++++++++++++++++++++++++++------------ plugins/BadWords/test.py | 47 ++++++++++++++++++++++++++-- 3 files changed, 94 insertions(+), 21 deletions(-) diff --git a/plugins/BadWords/config.py b/plugins/BadWords/config.py index 9c9fb31ee..de908cc46 100644 --- a/plugins/BadWords/config.py +++ b/plugins/BadWords/config.py @@ -119,6 +119,11 @@ class ReplacementMethods(registry.OnlySomeStrings): conf.registerChannelValue(BadWords, 'kick', registry.Boolean(False, _("""Determines whether the bot will kick people with a warning when they use bad words."""))) +conf.registerChannelValue(BadWords, 'redact', + registry.Boolean(False, _("""Determines whether the bot will redact + messages containing bad words (only on servers supporting IRCv3 + draft/message-redaction; requires supybot.protocols.irc.experimentalExtensions). + """))) conf.registerChannelValue(BadWords.kick, 'message', registry.NormalizedString(_("""You have been kicked for using a word prohibited in the presence of this bot. Please use more appropriate diff --git a/plugins/BadWords/plugin.py b/plugins/BadWords/plugin.py index a98e59320..c28c7cd35 100644 --- a/plugins/BadWords/plugin.py +++ b/plugins/BadWords/plugin.py @@ -1,7 +1,7 @@ ### # Copyright (c) 2002-2004, Jeremiah Fincher # Copyright (c) 2009, James McCoy -# Copyright (c) 2010-2021, Valentin Lorentz +# Copyright (c) 2010-2023, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -77,24 +77,49 @@ def inFilter(self, irc, msg): channel = msg.channel self.updateRegexp(channel, irc.network) s = ircutils.stripFormatting(msg.args[1]) - if irc.isChannel(channel) \ - and self.registryValue('kick', channel, irc.network): - if self.regexp.search(s): - c = irc.state.channels[channel] - cap = ircdb.makeChannelCapability(channel, 'op') - if c.isHalfopPlus(irc.nick): - if c.isHalfopPlus(msg.nick) or \ - ircdb.checkCapability(msg.prefix, cap): - self.log.debug("Not kicking %s from %s, because " - "they are halfop+ or can't be " - "kicked.", msg.nick, channel) - else: - message = self.registryValue('kick.message', - channel, irc.network) - irc.queueMsg(ircmsgs.kick(channel, msg.nick, message)) - else: - self.log.warning('Should kick %s from %s, but not opped.', - msg.nick, channel) + if not irc.isChannel(channel): + # not a channel, don't bother checking + return msg + + may_kick = self.registryValue('kick', channel, irc.network) + msgid = msg.server_tags.get("msgid") + may_redact = ( + "draft/message-redaction" in irc.state.capabilities_ack + and msgid + and conf.supybot.protocols.irc.experimentalExtensions() + and self.registryValue('redact', channel, irc.network) + ) + + if not may_kick and not may_redact: + # no configured action, don't bother checking + return msg + + if not self.regexp.search(s): + # message does not contain a bad word + return msg + + c = irc.state.channels[channel] + cap = ircdb.makeChannelCapability(channel, 'op') + if not c.isHalfopPlus(irc.nick): + self.log.warning( + 'Should kick and/or redact %s from %s, but not opped.', + msg.nick, channel) + return msg + + if may_redact: + irc.queueMsg(ircmsgs.IrcMsg( + command="REDACT", args=(channel, msgid))) + + if may_kick: + if c.isHalfopPlus(msg.nick) or \ + ircdb.checkCapability(msg.prefix, cap): + self.log.debug("Not kicking %s from %s, because " + "they are halfop+ or can't be " + "kicked.", msg.nick, channel) + else: + message = self.registryValue('kick.message', + channel, irc.network) + irc.queueMsg(ircmsgs.kick(channel, msg.nick, message)) return msg def updateRegexp(self, channel, network): diff --git a/plugins/BadWords/test.py b/plugins/BadWords/test.py index fa337ec6f..6ba9c1821 100644 --- a/plugins/BadWords/test.py +++ b/plugins/BadWords/test.py @@ -1,6 +1,6 @@ ### # Copyright (c) 2002-2004, Jeremiah Fincher -# Copyright (c) 2010-2021, Valentin Lorentz +# Copyright (c) 2010-2023, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -31,7 +31,7 @@ import supybot.conf as conf from supybot.test import * -class BadWordsTestCase(PluginTestCase): +class BadWordsOutfilterTestCase(PluginTestCase): plugins = ('BadWords', 'Utilities', 'Format', 'Filter') badwords = ('shit', 'ass', 'fuck') def tearDown(self): @@ -80,5 +80,48 @@ def testList(self): self.assertNotError('badwords add "fuck you"') self.assertResponse('badwords list', 'ass, fuck you, and shit') + +class BadWordsInfilterTestCase(ChannelPluginTestCase): + plugins = ('BadWords',) + badwords = ('shit', 'ass', 'fuck') + def tearDown(self): + # .default() doesn't seem to be working for BadWords.words + #default = conf.supybot.plugins.BadWords.words.default() + #conf.supybot.plugins.BadWords.words.setValue(default) + conf.supybot.plugins.BadWords.words.setValue([]) + + def testKick(self): + self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) + self.assertNotError('badwords add shit') + + with conf.supybot.plugins.BadWords.kick \ + .getSpecific(self.irc.network, self.channel).context(True): + self.irc.feedMsg(ircmsgs.privmsg(self.channel, + 'oh shit', + prefix='foobar!user@__no_testcap__')) + m = self.getMsg(' ') + self.assertIsNotNone(m) + self.assertEqual(m.command, 'KICK', m) + self.assertEqual(m.args[0], self.channel, m) + self.assertEqual(m.args[1], 'foobar', m) + + def testRedact(self): + self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) + self.irc.state.capabilities_ack.add('draft/message-redaction') + self.assertNotError('badwords add shit') + + with conf.supybot.plugins.BadWords.redact \ + .getSpecific(self.irc.network, self.channel).context(True): + with conf.supybot.protocols.irc.experimentalExtensions.context(True): + self.irc.feedMsg(ircmsgs.IrcMsg( + command='PRIVMSG', + args=(self.channel, 'oh shit'), + prefix='foobar!user@__no_testcap__', + server_tags={'msgid': 'abcde'})) + m = self.getMsg(' ', timeout=0.5) + self.assertIsNotNone(m) + self.assertEqual(m.command, 'REDACT', m) + self.assertEqual(m.args, (self.channel, 'abcde'), m) + # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: From 7b97f7fe2a264fb58063fa3ca21312f4a42aaad1 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 26 May 2023 21:30:56 +0200 Subject: [PATCH 3/4] Remove timeout override --- plugins/BadWords/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/BadWords/test.py b/plugins/BadWords/test.py index 6ba9c1821..920598616 100644 --- a/plugins/BadWords/test.py +++ b/plugins/BadWords/test.py @@ -118,7 +118,7 @@ def testRedact(self): args=(self.channel, 'oh shit'), prefix='foobar!user@__no_testcap__', server_tags={'msgid': 'abcde'})) - m = self.getMsg(' ', timeout=0.5) + m = self.getMsg(' ') self.assertIsNotNone(m) self.assertEqual(m.command, 'REDACT', m) self.assertEqual(m.args, (self.channel, 'abcde'), m) From df190e6aca8f792228a0cc8ec02f264e7e476335 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 26 May 2023 21:36:45 +0200 Subject: [PATCH 4/4] Channel: Add @redact command --- plugins/Channel/plugin.py | 49 +++++++++++++++++++++++++++- plugins/Channel/test.py | 68 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/plugins/Channel/plugin.py b/plugins/Channel/plugin.py index 26ad62501..9b66e7f61 100644 --- a/plugins/Channel/plugin.py +++ b/plugins/Channel/plugin.py @@ -1,7 +1,7 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009-2012, James McCoy -# Copyright (c) 2010-2021, Valentin Lorentz +# Copyright (c) 2010-2023, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -464,6 +464,53 @@ def unban(self, irc, msg, args, channel, hostmask): first('hostmask', ('literal', '--all')))]) + @internationalizeDocstring + def redact(self, irc, msg, args, channel, msgid, reason): + """[] [--msgid ] [] + + Redacts the message identified by the given from the + for . may be omitted if this command is given as a + reply to another message (on servers supporting IRCv3 message-tags + and +draft/reply). + This command requires supybot.protocols.irc.experimentalExtensions and + is only available on networks which support IRCv3 + draft/message-redaction. + is only necessary if the message isn't sent in the channel + itself. + """ + if not conf.supybot.protocols.irc.experimentalExtensions(): + irc.error( + _('Experimental IRC extensions are not enabled for this bot.'), + Raise=True) + + msgid = msgid or msg.server_tags.get('+draft/reply') + if not msgid: + irc.error(_('--msgid is required when this command is not a ' + 'reply to a message'), Raise=True) + + for target_message in reversed(irc.state.history): + if target_message.server_tags.get('msgid') == msgid: + break + else: + irc.error(_('Could not find the message in my history'), + Raise=True) + + if target_message.command not in ('PRIVMSG', 'NOTICE', 'TAGMSG'): + irc.error(_('This is not a valid message.'), + Raise=True) + + if ircutils.strEqual(target_message.nick, irc.nick): + irc.error(_('I cowardly refuse to redact my own messages.'), + Raise=True) + + args = [channel, msgid] + if reason: + args.append(reason) + self._sendMsg(irc, ircmsgs.IrcMsg(command="REDACT", args=args)) + redact = wrap(redact, ['op', ('haveHalfop+', _('kick someone')), + getopts({'msgid': 'somethingWithoutSpaces'}), + additional('text')]) + @internationalizeDocstring def listbans(self, irc, msg, args, channel): """[] diff --git a/plugins/Channel/test.py b/plugins/Channel/test.py index 4295ef081..b108f5f42 100644 --- a/plugins/Channel/test.py +++ b/plugins/Channel/test.py @@ -1,7 +1,7 @@ ### # Copyright (c) 2002-2005, Jeremiah Fincher # Copyright (c) 2009, James McCoy -# Copyright (c) 2010-2021, Valentin Lorentz +# Copyright (c) 2010-2023, Valentin Lorentz # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -289,5 +289,71 @@ def getAfterJoinMessages(): self.assertEqual(m.args[0], '#foo') self.assertEqual(m.args[1], 'reason') + def assertRedactNoMsgid(self, query, hostmask, **kwargs): + self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) + self.irc.state.capabilities_ack.add('draft/message-redaction') + + with conf.supybot.protocols.irc.experimentalExtensions.context(True): + self.irc.feedMsg(ircmsgs.IrcMsg( + command='PRIVMSG', + args=(self.channel, '@redact'))) + self.assertRegexp(' ', 'Error: --msgid is required when') + + def assertRedactMsgid(self, query, hostmask, **kwargs): + self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) + self.irc.state.capabilities_ack.add('draft/message-redaction') + + with conf.supybot.protocols.irc.experimentalExtensions.context(True): + self.irc.feedMsg(ircmsgs.IrcMsg( + command='PRIVMSG', + args=(self.channel, '@redact --msgid foobar'))) + self.assertRegexp(' ', 'Error: Cound not find') + + m = self.getMsg(' ', timeout=0.5) + self.assertIsNone(m) + + self.irc.feedMsg(ircmsgs.IrcMsg( + command='PRIVMSG', + args=(self.channel, 'oh shit'), + server_tags={'msgid': 'foobar'})) + + self.irc.feedMsg(ircmsgs.IrcMsg( + command='PRIVMSG', + args=(self.channel, '@redact --msgid foobar'))) + + m = self.getMsg(' ') + + self.assertEqual(m.command, 'REDACT') + self.assertEqual(m.args, (self.channel, 'foobar')) + + def assertRedactReply(self, query, hostmask, **kwargs): + self.irc.feedMsg(ircmsgs.op(self.channel, self.nick)) + self.irc.state.capabilities_ack.add('draft/message-redaction') + + with conf.supybot.protocols.irc.experimentalExtensions.context(True): + self.irc.feedMsg(ircmsgs.IrcMsg( + command='PRIVMSG', + args=(self.channel, '@redact'), + server_tags={'+draft/reply=foobar'})) + self.assertRegexp(' ', 'Error: Cound not find') + + m = self.getMsg(' ', timeout=0.5) + self.assertIsNone(m) + + self.irc.feedMsg(ircmsgs.IrcMsg( + command='PRIVMSG', + args=(self.channel, 'oh shit'), + server_tags={'msgid': 'foobar'})) + + self.irc.feedMsg(ircmsgs.IrcMsg( + command='PRIVMSG', + args=(self.channel, '@redact'), + server_tags={'+draft/reply=foobar'})) + + m = self.getMsg(' ') + + self.assertEqual(m.command, 'REDACT') + self.assertEqual(m.args, (self.channel, 'foobar')) + # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: