Skip to content

Implement IRCv3 draft/message-redaction #1538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions plugins/BadWords/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 44 additions & 19 deletions plugins/BadWords/plugin.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down
47 changes: 45 additions & 2 deletions plugins/BadWords/test.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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(' ')
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:

49 changes: 48 additions & 1 deletion plugins/Channel/plugin.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
"""[<channel>] [--msgid <msgid>] [<reason>]

Redacts the message identified by the given <msgid> from the <channel>
for <reason>. <msgid> 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.
<channel> 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):
"""[<channel>]
Expand Down
68 changes: 67 additions & 1 deletion plugins/Channel/test.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:

2 changes: 1 addition & 1 deletion src/irclib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down